Skip to content

Commit 00d2543

Browse files
committed
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 (cherry picked from commit 53f4f62)
1 parent 58caf17 commit 00d2543

File tree

5 files changed

+478
-1
lines changed

5 files changed

+478
-1
lines changed

specification/eslint.config.js

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,61 @@ export default defineConfig({
3636
'es-spec-validator/no-native-types': 'error',
3737
'es-spec-validator/invalid-node-types': 'error',
3838
'es-spec-validator/no-generic-number': 'error',
39-
'es-spec-validator/request-must-have-urls': 'error'
39+
'es-spec-validator/request-must-have-urls': 'error',
40+
'es-spec-validator/no-duplicate-type-names': [
41+
'error',
42+
{
43+
ignoreNames: ['Request', 'Response', 'ResponseBase'],
44+
existingDuplicates: {
45+
Action: [
46+
'indices.modify_data_stream',
47+
'indices.update_aliases',
48+
'watcher._types'
49+
],
50+
Actions: ['ilm._types', 'security.put_privileges', 'watcher._types'],
51+
ComponentTemplate: ['cat.component_templates', 'cluster._types'],
52+
Context: [
53+
'_global.get_script_context',
54+
'_global.search._types',
55+
'nodes._types'
56+
],
57+
DatabaseConfigurationMetadata: [
58+
'ingest.get_geoip_database',
59+
'ingest.get_ip_location_database'
60+
],
61+
Datafeed: ['ml._types', 'xpack.usage'],
62+
Destination: ['_global.reindex', 'transform._types'],
63+
Feature: ['features._types', 'indices.get', 'xpack.info'],
64+
Features: ['indices.get', 'xpack.info'],
65+
Filter: ['_global.termvectors', 'ml._types'],
66+
IndexingPressure: ['cluster.stats', 'indices._types', 'nodes._types'],
67+
IndexingPressureMemory: ['indices._types', 'nodes._types'],
68+
Ingest: ['ingest._types', 'nodes._types'],
69+
MigrationFeature: [
70+
'migration.get_feature_upgrade_status',
71+
'migration.post_feature_upgrade'
72+
],
73+
Operation: ['_global.mget', '_global.mtermvectors'],
74+
ResponseBody: ['_global.search', 'ml.evaluate_data_frame'],
75+
Phase: ['ilm._types', 'xpack.usage'],
76+
Phases: ['ilm._types', 'xpack.usage'],
77+
Pipeline: ['ingest._types', 'logstash._types'],
78+
Policy: ['enrich._types', 'ilm._types', 'slm._types'],
79+
RequestItem: ['_global.msearch', '_global.msearch_template'],
80+
ResponseItem: ['_global.bulk', '_global.mget', '_global.msearch'],
81+
RoleMapping: ['security._types', 'xpack.usage'],
82+
RuntimeFieldTypes: ['cluster.stats', 'xpack.usage'],
83+
ShardsStats: ['indices.field_usage_stats', 'snapshot._types'],
84+
ShardStats: ['ccr._types', 'indices.stats'],
85+
Source: ['_global.reindex', 'transform._types'],
86+
Token: [
87+
'_global.termvectors',
88+
'security.authenticate',
89+
'security.create_service_token',
90+
'security.enroll_kibana'
91+
]
92+
}
93+
}
94+
]
4095
}
4196
})

validator/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ It is configured [in the specification directory](../specification/eslint.config
1313
| `invalid-node-types` | The spec uses a subset of TypeScript, so some types, clauses and expressions are not allowed. |
1414
| `no-generic-number` | Generic `number` type is not allowed outside of `_types/Numeric.ts`. Use concrete numeric types like `integer`, `long`, `float`, `double`, etc. |
1515
| `request-must-have-urls` | All Request interfaces extending `RequestBase` must have a `urls` property defining their endpoint paths and HTTP methods. |
16+
| `no-duplicate-type-names` | All types must be unique across class and enum definitions. |
1617

1718
## Usage
1819

validator/eslint-plugin-es-spec.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import noNativeTypes from './rules/no-native-types.js'
2222
import invalidNodeTypes from './rules/invalid-node-types.js'
2323
import noGenericNumber from './rules/no-generic-number.js'
2424
import requestMustHaveUrls from './rules/request-must-have-urls.js'
25+
import noDuplicateTypeNames from './rules/no-duplicate-type-names.js'
2526

2627
export default {
2728
rules: {
@@ -31,5 +32,6 @@ export default {
3132
'invalid-node-types': invalidNodeTypes,
3233
'no-generic-number': noGenericNumber,
3334
'request-must-have-urls': requestMustHaveUrls,
35+
'no-duplicate-type-names': noDuplicateTypeNames
3436
}
3537
}
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)