Skip to content

Commit df8c7d1

Browse files
authored
Backport 5647 to 9.2 (#5815)
* chore: add eslint rule `no-duplicate-class-names` (#5647) * chore: add eslint rule `no-duplicate-class-names` * chore: remove redundant entry from IndexingPressureMemory in eslint config * chore: make contrib * fix merge * fix merge
1 parent 4d6bfc7 commit df8c7d1

File tree

5 files changed

+477
-0
lines changed

5 files changed

+477
-0
lines changed

specification/eslint.config.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,61 @@ export default defineConfig({
4040
'es-spec-validator/no-variants-on-responses': 'error',
4141
'es-spec-validator/no-inline-unions': 'error',
4242
'es-spec-validator/prefer-tagged-variants': 'error',
43+
'es-spec-validator/no-duplicate-type-names': [
44+
'error',
45+
{
46+
ignoreNames: ['Request', 'Response', 'ResponseBase'],
47+
existingDuplicates: {
48+
Action: [
49+
'indices.modify_data_stream',
50+
'indices.update_aliases',
51+
'watcher._types'
52+
],
53+
Actions: ['ilm._types', 'security.put_privileges', 'watcher._types'],
54+
ComponentTemplate: ['cat.component_templates', 'cluster._types'],
55+
Context: [
56+
'_global.get_script_context',
57+
'_global.search._types',
58+
'nodes._types'
59+
],
60+
DatabaseConfigurationMetadata: [
61+
'ingest.get_geoip_database',
62+
'ingest.get_ip_location_database'
63+
],
64+
Datafeed: ['ml._types', 'xpack.usage'],
65+
Destination: ['_global.reindex', 'transform._types'],
66+
Feature: ['features._types', 'indices.get', 'xpack.info'],
67+
Features: ['indices.get', 'xpack.info'],
68+
Filter: ['_global.termvectors', 'ml._types'],
69+
IndexingPressure: ['cluster.stats', 'indices._types', 'nodes._types'],
70+
IndexingPressureMemory: ['indices._types', 'nodes._types'],
71+
Ingest: ['ingest._types', 'nodes._types'],
72+
MigrationFeature: [
73+
'migration.get_feature_upgrade_status',
74+
'migration.post_feature_upgrade'
75+
],
76+
Operation: ['_global.mget', '_global.mtermvectors'],
77+
ResponseBody: ['_global.search', 'ml.evaluate_data_frame'],
78+
Phase: ['ilm._types', 'xpack.usage'],
79+
Phases: ['ilm._types', 'xpack.usage'],
80+
Pipeline: ['ingest._types', 'logstash._types'],
81+
Policy: ['enrich._types', 'ilm._types', 'slm._types'],
82+
RequestItem: ['_global.msearch', '_global.msearch_template'],
83+
ResponseItem: ['_global.bulk', '_global.mget', '_global.msearch'],
84+
RoleMapping: ['security._types', 'xpack.usage'],
85+
RuntimeFieldTypes: ['cluster.stats', 'xpack.usage'],
86+
ShardsStats: ['indices.field_usage_stats', 'snapshot._types'],
87+
ShardStats: ['ccr._types', 'indices.stats'],
88+
Source: ['_global.reindex', 'transform._types'],
89+
Token: [
90+
'_global.termvectors',
91+
'security.authenticate',
92+
'security.create_service_token',
93+
'security.enroll_kibana'
94+
]
95+
}
96+
}
97+
],
4398
'es-spec-validator/jsdoc-endpoint-check': [
4499
'error',
45100
{

validator/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ It is configured [in the specification directory](../specification/eslint.config
1616
| `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. |
1717
| `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. |
1818
| `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. |
19+
| `no-duplicate-type-names` | All types must be unique across class and enum definitions. |
1920
| `jsdoc-endpoint-check` | Validates JSDoc on endpoints in the specification. Ensuring consistent formatting. Some errors can be fixed with `--fix`. |
2021

2122
## Usage

validator/eslint-plugin-es-spec.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import requestMustHaveUrls from './rules/request-must-have-urls.js'
2525
import noVariantsOnResponses from './rules/no-variants-on-responses.js'
2626
import noInlineUnions from './rules/no-inline-unions.js'
2727
import preferTaggedVariants from './rules/prefer-tagged-variants.js'
28+
import noDuplicateTypeNames from './rules/no-duplicate-type-names.js'
2829
import jsdocEndpointCheck from './rules/jsdoc-endpoint-check.js'
2930

3031
export default {
@@ -38,6 +39,7 @@ export default {
3839
'no-variants-on-responses': noVariantsOnResponses,
3940
'no-inline-unions': noInlineUnions,
4041
'prefer-tagged-variants': preferTaggedVariants,
42+
'no-duplicate-type-names': noDuplicateTypeNames,
4143
'jsdoc-endpoint-check': jsdocEndpointCheck
4244
}
4345
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import { ESLintUtils } from '@typescript-eslint/utils';
20+
import ts from 'typescript';
21+
22+
/**
23+
* Formats location information for error messages
24+
* @param {Array} classes - Array of class locations
25+
* @returns {string} Formatted location string
26+
*/
27+
function formatLocation(classes) {
28+
const locations = classes.slice(0, 3).map(c =>
29+
`\n\t${c.fileName}:${c.line}:${c.column}`
30+
).join('');
31+
32+
const additionalCount = classes.length > 3 ? `\n ... and ${classes.length - 3} more` : '';
33+
const plural = classes.length > 1 ? 's' : '';
34+
35+
return `${classes.length} other location${plural}:${locations}${additionalCount}`;
36+
}
37+
38+
const typeCache = new Map();
39+
let clearCache = true;
40+
41+
function getNamespace(context) {
42+
const filename = context.filename;
43+
const specIndex = filename.indexOf('/specification/');
44+
45+
if (specIndex === -1) {
46+
return filename;
47+
}
48+
49+
const pathAfterSpec = filename.substring(specIndex + '/specification/'.length);
50+
return pathAfterSpec.split('/').slice(0, -1).join('.');
51+
}
52+
53+
export const noDuplicateTypeNamesFactory = (getParserServices) => {
54+
if (!getParserServices) {
55+
throw new Error('getParserServices is required');
56+
}
57+
return ESLintUtils.RuleCreator.withoutDocs(
58+
{
59+
name: 'no-duplicate-type-names',
60+
meta: {
61+
type: 'problem',
62+
docs: {
63+
description: 'Ensure type aliases and interfaces have unique names across the package',
64+
},
65+
messages: {
66+
duplicateTypeName: 'Duplicate: "{{typeName}}" is already declared in {{location}}\nRename this type to avoid conflicts with other types in the specification.'
67+
},
68+
schema: [
69+
{
70+
type: 'object',
71+
properties: {
72+
ignoreNames: {
73+
type: 'array',
74+
items: {
75+
type: 'string'
76+
},
77+
description: 'Array of type names to ignore when checking for duplicates'
78+
},
79+
existingDuplicates: {
80+
type: 'object',
81+
additionalProperties: {
82+
type: 'array',
83+
items: {
84+
type: 'string'
85+
}
86+
},
87+
description: 'Object mapping type names to arrays of locations where duplicates are expected'
88+
}
89+
},
90+
additionalProperties: false
91+
}
92+
],
93+
},
94+
defaultOptions: [{ ignoreNames: [], existingDuplicates: {} }],
95+
create(context) {
96+
const parserServices = getParserServices(context);
97+
const { ignoreNames, existingDuplicates } = context.options[0];
98+
const kinds = [
99+
ts.SyntaxKind.FunctionDeclaration,
100+
ts.SyntaxKind.ClassDeclaration,
101+
ts.SyntaxKind.InterfaceDeclaration,
102+
ts.SyntaxKind.TypeAliasDeclaration,
103+
ts.SyntaxKind.EnumDeclaration,
104+
];
105+
106+
if (clearCache) {
107+
typeCache.clear();
108+
clearCache = false;
109+
}
110+
111+
function findDuplicates(node) {
112+
try {
113+
if (!node.id || !node.id.name) {
114+
return;
115+
}
116+
117+
if (ignoreNames.includes(node.id.name)) {
118+
return;
119+
}
120+
121+
const existingDuplicatesForName = existingDuplicates[node.id.name];
122+
if (existingDuplicatesForName && existingDuplicatesForName.includes(getNamespace(context))) {
123+
return;
124+
}
125+
126+
let types = typeCache.get(node.id.name);
127+
if (!types) {
128+
types = [];
129+
parserServices.program.getSourceFiles().forEach(sourceFile => {
130+
if (sourceFile.fileName.includes('node_modules')) {
131+
return;
132+
}
133+
134+
sourceFile.forEachChild(fnDecl => {
135+
if (kinds.includes(fnDecl.kind)) {
136+
fnDecl.forEachChild(identifier => {
137+
if (identifier.kind === ts.SyntaxKind.Identifier &&
138+
identifier.escapedText === node.id.name) {
139+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(identifier.pos);
140+
types.push({
141+
typeName: identifier.escapedText,
142+
fileName: sourceFile.fileName,
143+
line: line + 1,
144+
column: character + 2
145+
});
146+
}
147+
});
148+
}
149+
});
150+
});
151+
typeCache.set(node.id.name, types);
152+
}
153+
154+
const filteredClasses = types.filter(c => c.fileName !== context.filename);
155+
if (filteredClasses.length > 0) {
156+
context.report({
157+
node: node.id,
158+
messageId: 'duplicateTypeName',
159+
data: {
160+
typeName: node.id.name,
161+
location: formatLocation(filteredClasses)
162+
}
163+
});
164+
}
165+
} catch (error) {
166+
console.error('ERROR in findDuplicates:', error.message, error.stack);
167+
throw error;
168+
}
169+
}
170+
171+
return {
172+
'ClassDeclaration, InterfaceDeclaration, TypedAliasDeclaration, TSEnumDeclaration, TSInterfaceDeclaration, TSTypeAliasDeclaration'(node) {
173+
findDuplicates(node);
174+
},
175+
}
176+
}
177+
}
178+
)
179+
}
180+
181+
export const noDuplicateTypeNames = noDuplicateTypeNamesFactory(ESLintUtils.getParserServices);
182+
183+
export default noDuplicateTypeNames;
184+
185+
export const ___testing_only = {
186+
cache: typeCache,
187+
setClearCache(value) {
188+
clearCache = value;
189+
}
190+
}

0 commit comments

Comments
 (0)