diff --git a/specification/eslint.config.js b/specification/eslint.config.js index 8c53878c57..ae7fa37ebd 100644 --- a/specification/eslint.config.js +++ b/specification/eslint.config.js @@ -36,6 +36,7 @@ export default defineConfig([ 'es-spec-validator/single-key-dictionary-key-is-string': 'error', 'es-spec-validator/dictionary-key-is-string': 'error', 'es-spec-validator/no-native-types': 'error', + 'es-spec-validator/no-same-name-as-enclosing-type': 'error', 'es-spec-validator/invalid-node-types': 'error', 'es-spec-validator/no-generic-number': 'error', 'es-spec-validator/codegen-exclude-on-request-only': 'error', diff --git a/specification/logstash/_types/Pipeline.ts b/specification/logstash/_types/Pipeline.ts index 547299ba08..bec8c1f3a5 100644 --- a/specification/logstash/_types/Pipeline.ts +++ b/specification/logstash/_types/Pipeline.ts @@ -53,6 +53,7 @@ export class PipelineSettings { */ 'queue.checkpoint.writes': integer } +// eslint-disable-next-line es-spec-validator/no-same-name-as-enclosing-type export class Pipeline { /** * A description of the pipeline. diff --git a/specification/ml/put_trained_model/types.ts b/specification/ml/put_trained_model/types.ts index 4d133c3b95..1477114bba 100644 --- a/specification/ml/put_trained_model/types.ts +++ b/specification/ml/put_trained_model/types.ts @@ -105,6 +105,7 @@ export class AggregateOutput { exponent?: Weights } +// eslint-disable-next-line es-spec-validator/no-same-name-as-enclosing-type export class Weights { weights: double } diff --git a/specification/nodes/_types/Stats.ts b/specification/nodes/_types/Stats.ts index 8790adda3d..2dda7fad7b 100644 --- a/specification/nodes/_types/Stats.ts +++ b/specification/nodes/_types/Stats.ts @@ -1072,6 +1072,7 @@ export class Scripting { contexts?: Context[] } +// eslint-disable-next-line es-spec-validator/no-same-name-as-enclosing-type export class Context { context?: string compilations?: long diff --git a/specification/security/put_privileges/types.ts b/specification/security/put_privileges/types.ts index bfd578ce5d..77fa82f3da 100644 --- a/specification/security/put_privileges/types.ts +++ b/specification/security/put_privileges/types.ts @@ -19,6 +19,7 @@ import { Metadata, Name } from '@_types/common' +// eslint-disable-next-line es-spec-validator/no-same-name-as-enclosing-type export class Actions { actions: string[] application?: string diff --git a/validator/README.md b/validator/README.md index daddc399f3..4808dde4b3 100644 --- a/validator/README.md +++ b/validator/README.md @@ -5,21 +5,22 @@ It is configured [in the specification directory](../specification/eslint.config ## Rules -| Name | Description | -|---------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `single-key-dictionary-key-is-string` | `SingleKeyDictionary` keys must be strings. | -| `dictionary-key-is-string` | `Dictionary` keys must be strings. | -| `no-native-types` | TypeScript native utility types (`Record`, `Partial`, etc.) and collection types (`Map`, `Set`, etc.) are not allowed. Use spec-defined aliases like `Dictionary` instead. | -| `invalid-node-types` | The spec uses a subset of TypeScript, so some types, clauses and expressions are not allowed. | -| `no-generic-number` | Generic `number` type is not allowed outside of `_types/Numeric.ts`. Use concrete numeric types like `integer`, `long`, `float`, `double`, etc. | -| `request-must-have-urls` | All Request interfaces extending `RequestBase` must have a `urls` property defining their endpoint paths and HTTP methods. | -| `no-variants-on-responses` | `@variants` is only supported on Interface types, not on Request or Response classes. Use value_body pattern with `@codegen_name` instead. Includes additional checks on variant tag use. | -| `no-inline-unions` | Inline union types (e.g., `field: A \| B`) are not allowed in properties/fields. Define a named type alias instead to improve code generation for statically-typed languages. | -| `prefer-tagged-variants` | Union of class types should use tagged variants (`@variants internal` or `@variants container`) instead of inline unions for better deserialization support in statically-typed languages. | -| `no-duplicate-type-names` | All types must be unique across class and enum definitions. | -| `no-all-string-literal-unions | Unions consisting entirely of string literals (e.g., `"green" \| "yellow" \| "red"`) are not allowed, use enums instead. | +| Name | Description | +|---------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `single-key-dictionary-key-is-string` | `SingleKeyDictionary` keys must be strings. | +| `dictionary-key-is-string` | `Dictionary` keys must be strings. | +| `no-native-types` | TypeScript native utility types (`Record`, `Partial`, etc.) and collection types (`Map`, `Set`, etc.) are not allowed. Use spec-defined aliases like `Dictionary` instead. | +| `no-same-name-as-enclosing-type` | Classes having fields with the same name as the class is breaking for some client libraries. | +| `invalid-node-types` | The spec uses a subset of TypeScript, so some types, clauses and expressions are not allowed. | +| `no-generic-number` | Generic `number` type is not allowed outside of `_types/Numeric.ts`. Use concrete numeric types like `integer`, `long`, `float`, `double`, etc. | +| `request-must-have-urls` | All Request interfaces extending `RequestBase` must have a `urls` property defining their endpoint paths and HTTP methods. | +| `no-variants-on-responses` | `@variants` is only supported on Interface types, not on Request or Response classes. Use value_body pattern with `@codegen_name` instead. Includes additional checks on variant tag use. | +| `no-inline-unions` | Inline union types (e.g., `field: A \| B`) are not allowed in properties/fields. Define a named type alias instead to improve code generation for statically-typed languages. | +| `prefer-tagged-variants` | Union of class types should use tagged variants (`@variants internal` or `@variants container`) instead of inline unions for better deserialization support in statically-typed languages. | +| `no-duplicate-type-names` | All types must be unique across class and enum definitions. | +| `no-all-string-literal-unions | Unions consisting entirely of string literals (e.g., `"green" \| "yellow" \| "red"`) are not allowed, use enums instead. | | `jsdoc-endpoint-check` | Validates JSDoc on endpoints in the specification. Ensuring consistent formatting. Some errors can be fixed with `--fix`. | -| `codegen-exclude-on-request-only` | Ensures `@codegen_exclude` is only used on request definitions located in namespaced `specification/` files (i.e. files. | +| `codegen-exclude-on-request-only` | Ensures `@codegen_exclude` is only used on request definitions located in namespaced `specification/` files (i.e. files. | ## Usage diff --git a/validator/eslint-plugin-es-spec.js b/validator/eslint-plugin-es-spec.js index 75e2717c6e..6f34d5fd16 100644 --- a/validator/eslint-plugin-es-spec.js +++ b/validator/eslint-plugin-es-spec.js @@ -19,6 +19,7 @@ import singleKeyDict from './rules/single-key-dictionary-key-is-string.js' import dict from './rules/dictionary-key-is-string.js' import noNativeTypes from './rules/no-native-types.js' +import noSameNameAsEnclosingType from './rules/no-same-name-as-enclosing-type.js' import invalidNodeTypes from './rules/invalid-node-types.js' import noGenericNumber from './rules/no-generic-number.js' import requestMustHaveUrls from './rules/request-must-have-urls.js' @@ -35,6 +36,7 @@ export default { 'single-key-dictionary-key-is-string': singleKeyDict, 'dictionary-key-is-string': dict, 'no-native-types': noNativeTypes, + 'no-same-name-as-enclosing-type': noSameNameAsEnclosingType, 'invalid-node-types': invalidNodeTypes, 'no-generic-number': noGenericNumber, 'request-must-have-urls': requestMustHaveUrls, diff --git a/validator/rules/no-same-name-as-enclosing-type.js b/validator/rules/no-same-name-as-enclosing-type.js new file mode 100644 index 0000000000..b90f2a7b69 --- /dev/null +++ b/validator/rules/no-same-name-as-enclosing-type.js @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import {ESLintUtils} from '@typescript-eslint/utils'; + +const createRule = ESLintUtils.RuleCreator(name => `https://example.com/rule/${name}`) + +/** + * Finds if selected field has a @codegen_name comment above + * @param comments - Array of comments from the class + * @param field - Definition of the field + * @returns {boolean} + */ +function commentAboveFieldHasCodegenName(comments, field) { + for (const comment of comments) { + if (comment.loc.end.line === field.loc.start.line-1) { // checking the comment is one line above the field + if (comment.value && comment.value.includes("@codegen_name")){ + const newName = comment.value.replaceAll("*","").trim().split(" ")[1] + if (newName !== field.name) return true + } + } + } + return false +} + +export default createRule({ + name: 'no-same-name-as-enclosing-type', + create(context) { + return { + ClassDeclaration(node) { + if (!node.id || !node.id.name) { + return; // anonymous class - nothing to check + } + const services = ESLintUtils.getParserServices(context) + const tsClass = services.esTreeNodeToTSNodeMap.get(node); + if (!tsClass || !tsClass.members) { + return; // no fields + } + const className = node.id.name; + for (const element of node.body.body) { + if (element.type === "PropertyDefinition" && element.key.name && element.key.type === "Identifier") { + const fieldName = element.key.name + const field = element.key; + if (String(fieldName).toUpperCase() === className.toUpperCase()) { + const sourceCode = context.getSourceCode(); + const commentsInside = sourceCode.getCommentsInside(node) + if (!commentsInside || commentsInside.length === 0 || !commentAboveFieldHasCodegenName(commentsInside, field)) { + context.report({ + node, + messageId: 'shouldNotUseClassNameForFieldNames', + data: { + class: className, + suggestion: 'Either change the class name, or if it\'s not possible, use a different codegen name for the field by commenting it with /** @codegen_name different_name **/.' + } + }) + } + } + } + } + } + , + } + }, + meta: + { + docs: { + description: 'Classes having fields with the same name as the class is breaking for some client libraries.' + } + , + messages: { + shouldNotUseClassNameForFieldNames: 'Class "{{class}}" has invalid fields. {{suggestion}}.' + } + , + type: 'suggestion', + } + , + defaultOptions: [] +}) diff --git a/validator/test/no-same-name-as-enclosing-type.js b/validator/test/no-same-name-as-enclosing-type.js new file mode 100644 index 0000000000..6144efd3c2 --- /dev/null +++ b/validator/test/no-same-name-as-enclosing-type.js @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { RuleTester } from '@typescript-eslint/rule-tester' +import rule from '../rules/no-same-name-as-enclosing-type.js' + +const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { + projectService: { + allowDefaultProject: ['*.ts*'], + }, + tsconfigRootDir: import.meta.dirname, + }, + }, +}) + +ruleTester.run('no-same-name-as-enclosing-type', rule, { + valid: [ + `class MyClass { + field: integer + anotherfield: string + }`, + `class MyClass { + /** @codegen_name new_name **/ + myclass: integer + }` + ], + invalid: [ + { + code: `class MyClass { MyClass: integer }`, + errors: [{ messageId: 'shouldNotUseClassNameForFieldNames' }] + }, + { + code: `class MyClass { myclass: integer }`, + errors: [{ messageId: 'shouldNotUseClassNameForFieldNames' }] + }, + { + code: `class MyClass { + field: integer + anotherfield: string + myclass: boolean + }`, + errors: [{ messageId: 'shouldNotUseClassNameForFieldNames' }] + }, + { + code: `class MyClass { + field: integer + anotherfield: string + /** @codegen_name myclass **/ + myclass: boolean + }`, + errors: [{ messageId: 'shouldNotUseClassNameForFieldNames' }] + } + ], +})