@terra-symposium/standards-quality 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -2
- package/dist/collapse.d.ts +13 -0
- package/dist/collapse.d.ts.map +1 -0
- package/dist/collapse.js +151 -0
- package/dist/collapse.js.map +1 -0
- package/dist/documentation.d.ts +84 -0
- package/dist/documentation.d.ts.map +1 -0
- package/dist/documentation.js +1244 -0
- package/dist/documentation.js.map +1 -0
- package/dist/files.d.ts +39 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +90 -0
- package/dist/files.js.map +1 -0
- package/dist/github-report.d.ts +89 -0
- package/dist/github-report.d.ts.map +1 -0
- package/dist/github-report.js +469 -0
- package/dist/github-report.js.map +1 -0
- package/dist/history-report.d.ts +52 -0
- package/dist/history-report.d.ts.map +1 -0
- package/dist/history-report.js +317 -0
- package/dist/history-report.js.map +1 -0
- package/dist/imports.d.ts +19 -0
- package/dist/imports.d.ts.map +1 -0
- package/dist/imports.js +90 -0
- package/dist/imports.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/inline-if.d.ts +13 -0
- package/dist/inline-if.d.ts.map +1 -0
- package/dist/inline-if.js +48 -0
- package/dist/inline-if.js.map +1 -0
- package/dist/naming.d.ts +148 -0
- package/dist/naming.d.ts.map +1 -0
- package/dist/naming.js +642 -0
- package/dist/naming.js.map +1 -0
- package/dist/object-forwarding.d.ts +44 -0
- package/dist/object-forwarding.d.ts.map +1 -0
- package/dist/object-forwarding.js +182 -0
- package/dist/object-forwarding.js.map +1 -0
- package/dist/patterns.d.ts +85 -0
- package/dist/patterns.d.ts.map +1 -0
- package/dist/patterns.js +905 -0
- package/dist/patterns.js.map +1 -0
- package/dist/port.d.ts +21 -0
- package/dist/port.d.ts.map +1 -0
- package/dist/port.js +81 -0
- package/dist/port.js.map +1 -0
- package/dist/progress.d.ts +16 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +30 -0
- package/dist/progress.js.map +1 -0
- package/dist/runner.d.ts +257 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +2481 -0
- package/dist/runner.js.map +1 -0
- package/dist/security.d.ts +19 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +76 -0
- package/dist/security.js.map +1 -0
- package/dist/single-use.d.ts +60 -0
- package/dist/single-use.d.ts.map +1 -0
- package/dist/single-use.js +1209 -0
- package/dist/single-use.js.map +1 -0
- package/package.json +45 -5
- package/quality.schema.json +130 -0
package/dist/naming.js
ADDED
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* packages/quality/src/naming.ts
|
|
3
|
+
* Renames identifiers according to the configured replacement rules.
|
|
4
|
+
*/
|
|
5
|
+
/*//////////////////////////////////////////////////
|
|
6
|
+
IMPORTS
|
|
7
|
+
//////////////////////////////////////////////////*/
|
|
8
|
+
// API
|
|
9
|
+
import ts from 'typescript';
|
|
10
|
+
import { displayPath, srcFiles } from './files.js';
|
|
11
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
12
|
+
/*//////////////////////////////////////////////////
|
|
13
|
+
REGEX
|
|
14
|
+
//////////////////////////////////////////////////*/
|
|
15
|
+
const A_Z_REGEX = /^[A-Z]/;
|
|
16
|
+
export const IDENTIFIER_REPLACEMENTS = replacementMap([
|
|
17
|
+
[['administrator', 'administration'], 'admin'],
|
|
18
|
+
['attribute', 'attr'],
|
|
19
|
+
['category', 'cat'],
|
|
20
|
+
['intelligence', 'intel'],
|
|
21
|
+
['current', 'curr'],
|
|
22
|
+
['command', 'cmd'],
|
|
23
|
+
['column', 'col'],
|
|
24
|
+
['context', 'ctx'],
|
|
25
|
+
[['config', 'configuration'], 'cfg'],
|
|
26
|
+
['configured', 'cfgd'],
|
|
27
|
+
[['compare', 'comp'], 'comp'],
|
|
28
|
+
['definition', 'def'],
|
|
29
|
+
['difference', 'diff'],
|
|
30
|
+
['directory', 'dir'],
|
|
31
|
+
['destination', 'dest'],
|
|
32
|
+
['error', 'err'],
|
|
33
|
+
['event', 'e'],
|
|
34
|
+
['expression', 'expr'],
|
|
35
|
+
['extension', 'ext'],
|
|
36
|
+
[['format', 'formatted', 'formatting', 'formatter'], 'fmt'],
|
|
37
|
+
['function', 'fn'],
|
|
38
|
+
[['generation', 'generate', 'generated', 'generating'], 'gen'],
|
|
39
|
+
['image', 'img'],
|
|
40
|
+
[['identify', 'indentifier'], 'id'],
|
|
41
|
+
['index', 'idx'],
|
|
42
|
+
[['initialise', 'initialised', 'initialises', 'initialising', 'initialisation', 'initialize', 'initialized', 'initializes', 'initializing', 'initialization'], 'init'],
|
|
43
|
+
[['initialiser', 'initializer'], 'init'],
|
|
44
|
+
['language', 'lang'],
|
|
45
|
+
['member', 'mem'],
|
|
46
|
+
['message', 'msg'],
|
|
47
|
+
[['organization', 'organisation'], 'org'],
|
|
48
|
+
['operation', 'op'],
|
|
49
|
+
['option', 'opt'],
|
|
50
|
+
['parameter', 'param'],
|
|
51
|
+
['previous', 'prev'],
|
|
52
|
+
['position', 'pos'],
|
|
53
|
+
['record', 'rec'],
|
|
54
|
+
['repository', 'repo'],
|
|
55
|
+
[['request', 'requested'], 'req'],
|
|
56
|
+
['response', 'res'],
|
|
57
|
+
['source', 'src'],
|
|
58
|
+
['target', 'tgt'],
|
|
59
|
+
['value', 'val'],
|
|
60
|
+
]);
|
|
61
|
+
export const PREFIX_REPLACEMENTS = replacementMap([
|
|
62
|
+
['active', 'a'],
|
|
63
|
+
['next', 'n'],
|
|
64
|
+
['safe', 's'],
|
|
65
|
+
[['normalise', 'normalised', 'normalises', 'normalize', 'normalized', 'normalizes'], 'norm'],
|
|
66
|
+
[['clean', 'cleaned', 'cleaner'], 'c'],
|
|
67
|
+
[['calculate', 'calculated'], 'calc'],
|
|
68
|
+
[['validate', 'validated', 'validation'], 'valid'],
|
|
69
|
+
[['sanitize', 'sanitized'], 'san'],
|
|
70
|
+
]);
|
|
71
|
+
export const CALLBACK_ALIASES = replacementMap([
|
|
72
|
+
['workshop', 'w'],
|
|
73
|
+
[['customer', 'candidate', 'column'], 'c'],
|
|
74
|
+
[['opportunity', 'organization', 'organisation', 'option'], 'o'],
|
|
75
|
+
['member', 'm'],
|
|
76
|
+
[['record', 'result', 'row'], 'r'],
|
|
77
|
+
[['stage', 'section'], 's'],
|
|
78
|
+
['item', 'i'],
|
|
79
|
+
['index', 'i'],
|
|
80
|
+
[['entry', 'event', 'edge'], 'e'],
|
|
81
|
+
[['value', 'intent'], 'v'],
|
|
82
|
+
['group', 'g'],
|
|
83
|
+
['file', 'f'],
|
|
84
|
+
['problem', 'p'],
|
|
85
|
+
['node', 'n'],
|
|
86
|
+
['link', 'l'],
|
|
87
|
+
['data', 'd'],
|
|
88
|
+
], false);
|
|
89
|
+
export const CALLBACK_METHODS = new Set([
|
|
90
|
+
'addEventListener',
|
|
91
|
+
'every',
|
|
92
|
+
'filter',
|
|
93
|
+
'find',
|
|
94
|
+
'findIndex',
|
|
95
|
+
'findLast',
|
|
96
|
+
'findLastIndex',
|
|
97
|
+
'flatMap',
|
|
98
|
+
'forEach',
|
|
99
|
+
'forEachChild',
|
|
100
|
+
'map',
|
|
101
|
+
'reduce',
|
|
102
|
+
'reduceRight',
|
|
103
|
+
'some',
|
|
104
|
+
'sort',
|
|
105
|
+
]);
|
|
106
|
+
export const PROTECTED_IDENTIFIERS = new Set([
|
|
107
|
+
'GET',
|
|
108
|
+
'HEAD',
|
|
109
|
+
'OPTIONS',
|
|
110
|
+
'POST',
|
|
111
|
+
'PUT',
|
|
112
|
+
'PATCH',
|
|
113
|
+
'DELETE',
|
|
114
|
+
'default',
|
|
115
|
+
'FormData',
|
|
116
|
+
'Headers',
|
|
117
|
+
'NextRequest',
|
|
118
|
+
'NextResponse',
|
|
119
|
+
'Request',
|
|
120
|
+
'Response',
|
|
121
|
+
'URL',
|
|
122
|
+
'URLSearchParams',
|
|
123
|
+
'WebSocket',
|
|
124
|
+
'CarouselContextProps',
|
|
125
|
+
'ChartContextProps',
|
|
126
|
+
'FormSelectOptionProps',
|
|
127
|
+
'SidebarContextProps',
|
|
128
|
+
]);
|
|
129
|
+
export const PROTECTED_PATH_PARTS = new Set(['node_modules', '.next', 'out', 'build', 'coverage', 'src/components/ui']);
|
|
130
|
+
export const PROTECTED_FILENAMES = new Set(['page.tsx', 'page.ts', 'layout.tsx', 'layout.ts', 'route.ts', 'route.tsx', 'error.tsx', 'loading.tsx', 'not-found.tsx', 'index.ts', 'index.tsx']);
|
|
131
|
+
export const CODE_EXTS = new Set(['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts']);
|
|
132
|
+
const aliasTgt = (n) => ts.isIdentifier(n) || ts.isPropertyAccessExpression(n) || ts.isElementAccessExpression(n) ? n : undefined;
|
|
133
|
+
/*//////////////////////////////////////////////////
|
|
134
|
+
HELPERS
|
|
135
|
+
//////////////////////////////////////////////////*/
|
|
136
|
+
/**
|
|
137
|
+
* Builds a frozen lookup table of long identifier words and their concise replacements.
|
|
138
|
+
*
|
|
139
|
+
* @param groups Replacement groups where each left-hand word maps to one replacement.
|
|
140
|
+
* @param pluralReplacements Whether inferred plural keys should receive plural replacements.
|
|
141
|
+
*
|
|
142
|
+
* @return Immutable replacement map including supported plural forms.
|
|
143
|
+
*/
|
|
144
|
+
function replacementMap(groups, pluralReplacements = true) {
|
|
145
|
+
const explicit = new Map(groups.flatMap(([n, r]) => (typeof n === 'string' ? [n] : n).map((n) => [n, r])));
|
|
146
|
+
const plural = (word) => {
|
|
147
|
+
if (/[^aeiou]y$/i.test(word))
|
|
148
|
+
return `${word.slice(0, -1)}ies`;
|
|
149
|
+
if (/(?:s|x|z|ch|sh)$/i.test(word))
|
|
150
|
+
return word.length <= 4 ? word : `${word}es`;
|
|
151
|
+
return `${word}s`;
|
|
152
|
+
};
|
|
153
|
+
const entries = [...explicit];
|
|
154
|
+
for (const [name, replacement] of explicit) {
|
|
155
|
+
const pluralName = plural(name);
|
|
156
|
+
if (!explicit.has(pluralName))
|
|
157
|
+
entries.push([pluralName, pluralReplacements ? plural(replacement) : replacement]);
|
|
158
|
+
}
|
|
159
|
+
return Object.freeze(Object.fromEntries(entries));
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Splits an identifier into words while preserving acronym and numeric boundaries.
|
|
163
|
+
*
|
|
164
|
+
* @param name Identifier to tokenize.
|
|
165
|
+
*
|
|
166
|
+
* @return Identifier word tokens in source order.
|
|
167
|
+
*/
|
|
168
|
+
export function identifierTokens(name) {
|
|
169
|
+
return name
|
|
170
|
+
.split(/[_-]/)
|
|
171
|
+
.flatMap((p) => p.match(/[A-Z]+(?=[A-Z][a-z]|\d|$)|[A-Z]?[a-z]+|\d+/g) ?? [p])
|
|
172
|
+
.filter(Boolean);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Reassembles replacement tokens using the original identifier's casing and separators.
|
|
176
|
+
*
|
|
177
|
+
* @param original Identifier whose formatting should be preserved.
|
|
178
|
+
* @param tokens Replacement tokens to render.
|
|
179
|
+
*
|
|
180
|
+
* @return The replacement identifier in the original naming style.
|
|
181
|
+
*/
|
|
182
|
+
function renderIdentifier(original, tokens) {
|
|
183
|
+
if (/^[A-Z][A-Z0-9_]*$/.test(original))
|
|
184
|
+
return tokens.join(original.includes('_') ? '_' : '').toUpperCase();
|
|
185
|
+
if (original.includes('_'))
|
|
186
|
+
return tokens.join('_');
|
|
187
|
+
if (original.includes('-'))
|
|
188
|
+
return tokens.join('-');
|
|
189
|
+
if (A_Z_REGEX.test(original))
|
|
190
|
+
return tokens.map((t) => t.charAt(0).toUpperCase() + t.slice(1)).join('');
|
|
191
|
+
return tokens.map((t, i) => (i === 0 ? t.toLowerCase() : t.charAt(0).toUpperCase() + t.slice(1))).join('');
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Applies the original token's uppercase, PascalCase, or lowercase style to a replacement word.
|
|
195
|
+
*
|
|
196
|
+
* @param original Original identifier token.
|
|
197
|
+
* @param replacement Concise replacement token.
|
|
198
|
+
*
|
|
199
|
+
* @return Replacement token with casing matching the original.
|
|
200
|
+
*/
|
|
201
|
+
function tokenCase(original, replacement) {
|
|
202
|
+
if (original === original.toUpperCase())
|
|
203
|
+
return replacement.toUpperCase();
|
|
204
|
+
if (original.charAt(0) === original.charAt(0).toUpperCase())
|
|
205
|
+
return replacement.charAt(0).toUpperCase() + replacement.slice(1);
|
|
206
|
+
return replacement;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Chooses the preferred one-letter alias for a callback parameter without colliding with local names.
|
|
210
|
+
*
|
|
211
|
+
* @param name Existing callback parameter name.
|
|
212
|
+
* @param reserved Names already used in the callback scope.
|
|
213
|
+
*
|
|
214
|
+
* @return A concise available callback alias.
|
|
215
|
+
*/
|
|
216
|
+
export function callbackAlias(name, reserved = []) {
|
|
217
|
+
const cfgd = CALLBACK_ALIASES[name.toLowerCase()];
|
|
218
|
+
if (!cfgd)
|
|
219
|
+
return name;
|
|
220
|
+
if (!reserved.includes(cfgd))
|
|
221
|
+
return cfgd;
|
|
222
|
+
for (const fallback of ['v', 'r', 'x', 'd', 'n', 'e', 'c', 's'])
|
|
223
|
+
if (!reserved.includes(fallback))
|
|
224
|
+
return fallback;
|
|
225
|
+
return cfgd;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Produces the configured concise form of an identifier, including callback and numeric-index aliases.
|
|
229
|
+
*
|
|
230
|
+
* @param name Identifier to shorten.
|
|
231
|
+
* @param kind Context flags controlling callback, index, and reserved-name handling.
|
|
232
|
+
*
|
|
233
|
+
* @return Concise identifier, or the original name when no safe replacement applies.
|
|
234
|
+
*/
|
|
235
|
+
export function renameId(name, kind = {}) {
|
|
236
|
+
if (PROTECTED_IDENTIFIERS.has(name) || name === '_')
|
|
237
|
+
return name;
|
|
238
|
+
if (kind.numericIndex && name === 'index')
|
|
239
|
+
return callbackAlias('item', kind.reserved);
|
|
240
|
+
if (kind.callback)
|
|
241
|
+
return callbackAlias(name, kind.reserved);
|
|
242
|
+
const tokens = identifierTokens(name);
|
|
243
|
+
if (!tokens.some((t) => Object.hasOwn(IDENTIFIER_REPLACEMENTS, t.toLowerCase()) || Object.hasOwn(PREFIX_REPLACEMENTS, t.toLowerCase())))
|
|
244
|
+
return name;
|
|
245
|
+
return renderIdentifier(name, tokens.map((t, i) => {
|
|
246
|
+
const lower = t.toLowerCase();
|
|
247
|
+
const replacement = i === 0 && Object.hasOwn(PREFIX_REPLACEMENTS, lower)
|
|
248
|
+
? PREFIX_REPLACEMENTS[lower]
|
|
249
|
+
: (IDENTIFIER_REPLACEMENTS[lower] ?? t);
|
|
250
|
+
return replacement === t ? t : tokenCase(t, replacement);
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Determines whether an identifier parameter belongs to an inline or named callback.
|
|
255
|
+
*
|
|
256
|
+
* @param n Parameter identifier to classify.
|
|
257
|
+
*
|
|
258
|
+
* @return Callback rename context when the parameter is safely concise-able; otherwise `undefined`.
|
|
259
|
+
*/
|
|
260
|
+
function callbackKind(n) {
|
|
261
|
+
// JSX component bindings and explicit destructuring aliases encode syntax or
|
|
262
|
+
// public property names; shortening them can produce invalid JSX or change
|
|
263
|
+
// the inferred shape of a discriminated union.
|
|
264
|
+
if (A_Z_REGEX.test(n.text) || (ts.isBindingElement(n.parent) && n.parent.propertyName))
|
|
265
|
+
return undefined;
|
|
266
|
+
const param = ts.findAncestor(n, ts.isParameter);
|
|
267
|
+
if (!param)
|
|
268
|
+
return undefined;
|
|
269
|
+
const callback = ts.findAncestor(param, (n) => ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n));
|
|
270
|
+
if (!callback)
|
|
271
|
+
return undefined;
|
|
272
|
+
const isInlineCallback = !ts.isFunctionDeclaration(callback) && ts.isCallExpression(callback.parent) && callback.parent.arguments.includes(callback);
|
|
273
|
+
let isNamedCallback = false;
|
|
274
|
+
if (ts.isFunctionDeclaration(callback) && callback.name) {
|
|
275
|
+
const scan = (n) => {
|
|
276
|
+
if (isNamedCallback)
|
|
277
|
+
return;
|
|
278
|
+
const jsxAttr = ts.isJsxAttribute(n) ? n : undefined;
|
|
279
|
+
const jsxExpr = jsxAttr?.initializer && ts.isJsxExpression(jsxAttr.initializer) ? jsxAttr.initializer.expression : undefined;
|
|
280
|
+
const isJsxHandler = !!jsxAttr &&
|
|
281
|
+
ts.isIdentifier(jsxAttr.name) &&
|
|
282
|
+
/^on[A-Z]/.test(jsxAttr.name.text) &&
|
|
283
|
+
!!jsxExpr &&
|
|
284
|
+
(() => {
|
|
285
|
+
let refs = false;
|
|
286
|
+
const findRef = (c) => {
|
|
287
|
+
if (refs)
|
|
288
|
+
return;
|
|
289
|
+
if (ts.isIdentifier(c) && c.text === callback.name?.text) {
|
|
290
|
+
refs = true;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
ts.forEachChild(c, findRef);
|
|
294
|
+
};
|
|
295
|
+
findRef(jsxExpr);
|
|
296
|
+
return refs;
|
|
297
|
+
})();
|
|
298
|
+
if (ts.isCallExpression(n) &&
|
|
299
|
+
ts.isPropertyAccessExpression(n.expression) &&
|
|
300
|
+
CALLBACK_METHODS.has(n.expression.name.text) &&
|
|
301
|
+
n.arguments.some((a) => ts.isIdentifier(a) && a.text === callback.name?.text)) {
|
|
302
|
+
isNamedCallback = true;
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (isJsxHandler) {
|
|
306
|
+
isNamedCallback = true;
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
ts.forEachChild(n, scan);
|
|
310
|
+
};
|
|
311
|
+
scan(callback.parent);
|
|
312
|
+
}
|
|
313
|
+
const isJsxCallback = ts.isJsxExpression(callback.parent) && callback.parent.expression === callback && ts.isJsxAttribute(callback.parent.parent);
|
|
314
|
+
if (!isInlineCallback && !isNamedCallback && !isJsxCallback)
|
|
315
|
+
return undefined;
|
|
316
|
+
const declarations = [];
|
|
317
|
+
const collectBinding = (name) => {
|
|
318
|
+
if (ts.isIdentifier(name))
|
|
319
|
+
declarations.push(name);
|
|
320
|
+
else
|
|
321
|
+
for (const element of name.elements)
|
|
322
|
+
if (ts.isBindingElement(element))
|
|
323
|
+
collectBinding(element.name);
|
|
324
|
+
};
|
|
325
|
+
for (const p of callback.parameters)
|
|
326
|
+
collectBinding(p.name);
|
|
327
|
+
const declarationNames = new Set(declarations.map((d) => d.text));
|
|
328
|
+
const reserved = new Set();
|
|
329
|
+
const collect = (child) => {
|
|
330
|
+
if (ts.isIdentifier(child) && !declarationNames.has(child.text))
|
|
331
|
+
reserved.add(child.text);
|
|
332
|
+
ts.forEachChild(child, collect);
|
|
333
|
+
};
|
|
334
|
+
collect(callback);
|
|
335
|
+
for (const declaration of declarations) {
|
|
336
|
+
if (declaration === n)
|
|
337
|
+
return { callback: true, numericIndex: callback.parameters.indexOf(param) === 1 && n.text === 'index', reserved: [...reserved] };
|
|
338
|
+
const declarationParam = ts.findAncestor(declaration, ts.isParameter);
|
|
339
|
+
reserved.add(renameId(declaration.text, { callback: true, numericIndex: !!declarationParam && callback.parameters.indexOf(declarationParam) === 1 && declaration.text === 'index', reserved: [...reserved] }));
|
|
340
|
+
}
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Returns whether a declaration belongs to a supported callback.
|
|
345
|
+
*
|
|
346
|
+
* @param node Identifier declaration to inspect.
|
|
347
|
+
*
|
|
348
|
+
* @return `true` when the declaration is a supported callback parameter.
|
|
349
|
+
*/
|
|
350
|
+
export function isCallbackDeclaration(node) {
|
|
351
|
+
return callbackKind(node) !== undefined;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Returns the context-aware concise suggestion for one repository-owned declaration.
|
|
355
|
+
*
|
|
356
|
+
* @param n Identifier declaration whose context determines the suggestion.
|
|
357
|
+
*
|
|
358
|
+
* @return Concise name appropriate for the declaration's scope and callback role.
|
|
359
|
+
*/
|
|
360
|
+
export function renameDeclaration(n) {
|
|
361
|
+
return renameId(n.text, callbackKind(n));
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Checks whether an identifier starts with a configured prefix replacement.
|
|
365
|
+
*
|
|
366
|
+
* @param name Identifier to inspect.
|
|
367
|
+
*
|
|
368
|
+
* @return `true` when its first token has a configured concise prefix.
|
|
369
|
+
*/
|
|
370
|
+
export function hasPrefixReplacement(name) {
|
|
371
|
+
return Object.hasOwn(PREFIX_REPLACEMENTS, identifierTokens(name)[0]?.toLowerCase() ?? '');
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Checks whether a path is protected from symbol renaming because it is generated or framework-owned.
|
|
375
|
+
*
|
|
376
|
+
* @param fileName Path to inspect.
|
|
377
|
+
*
|
|
378
|
+
* @return `true` for declaration files and configured protected path segments.
|
|
379
|
+
*/
|
|
380
|
+
export function isProtectedPath(fileName) {
|
|
381
|
+
const norm = fileName.replaceAll('\\', '/');
|
|
382
|
+
return (norm.endsWith('.d.ts') ||
|
|
383
|
+
[...PROTECTED_PATH_PARTS].some((p) => (p.includes('/') ? norm === p || norm.endsWith(`/${p}`) || norm.startsWith(`${p}/`) || norm.includes(`/${p}/`) : norm.split('/').includes(p))));
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Checks whether a filename is a framework convention whose declarations must retain their names.
|
|
387
|
+
*
|
|
388
|
+
* @param fileName Path or basename to inspect.
|
|
389
|
+
*
|
|
390
|
+
* @return `true` when the basename is a protected route or component filename.
|
|
391
|
+
*/
|
|
392
|
+
export function isProtectedFilename(fileName) {
|
|
393
|
+
return PROTECTED_FILENAMES.has(fileName.split(/[\\/]/).at(-1) ?? fileName);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Reports whether an identifier already satisfies the configured concise-name rules.
|
|
397
|
+
*
|
|
398
|
+
* @param name Identifier to validate.
|
|
399
|
+
* @param kind Context flags used when the identifier is a callback or index parameter.
|
|
400
|
+
*
|
|
401
|
+
* @return `true` when renaming would produce the same identifier.
|
|
402
|
+
*/
|
|
403
|
+
export function isApprovedId(name, kind = {}) {
|
|
404
|
+
return renameId(name, kind) === name;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Returns whether an identifier declares a repository-owned symbol safe to rename with all references.
|
|
408
|
+
*
|
|
409
|
+
* @param n Identifier declaration to classify.
|
|
410
|
+
*
|
|
411
|
+
* @return `true` when all references can be updated as a repository-local rename.
|
|
412
|
+
*/
|
|
413
|
+
export function isRenamableDeclaration(n) {
|
|
414
|
+
if (ts.isParameter(n.parent) &&
|
|
415
|
+
n.parent.modifiers?.some((m) => [ts.SyntaxKind.PublicKeyword, ts.SyntaxKind.PrivateKeyword, ts.SyntaxKind.ProtectedKeyword, ts.SyntaxKind.ReadonlyKeyword].includes(m.kind)))
|
|
416
|
+
return false;
|
|
417
|
+
const declaration = ts.findAncestor(n.parent, (a) => ts.isVariableDeclaration(a) || ts.isParameter(a));
|
|
418
|
+
const varStatement = declaration && ts.isVariableDeclaration(declaration) && ts.isVariableDeclarationList(declaration.parent) && ts.isVariableStatement(declaration.parent.parent)
|
|
419
|
+
? declaration.parent.parent
|
|
420
|
+
: undefined;
|
|
421
|
+
return ((ts.isVariableDeclaration(n.parent) && n.parent.name === n && !varStatement?.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) ||
|
|
422
|
+
(ts.isParameter(n.parent) && n.parent.name === n) ||
|
|
423
|
+
(ts.isBindingElement(n.parent) && n.parent.name === n && !!declaration && !varStatement?.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) ||
|
|
424
|
+
(ts.isFunctionDeclaration(n.parent) && n.parent.name === n && !n.parent.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) ||
|
|
425
|
+
(ts.isFunctionExpression(n.parent) && n.parent.name === n) ||
|
|
426
|
+
(ts.isClassDeclaration(n.parent) && n.parent.name === n && !n.parent.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) ||
|
|
427
|
+
(ts.isClassExpression(n.parent) && n.parent.name === n) ||
|
|
428
|
+
(ts.isInterfaceDeclaration(n.parent) && n.parent.name === n && !n.parent.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) ||
|
|
429
|
+
(ts.isTypeAliasDeclaration(n.parent) && n.parent.name === n && !n.parent.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)));
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Returns the function or source-file scope that owns a declaration.
|
|
433
|
+
*
|
|
434
|
+
* @param n Identifier whose owning function or source-file scope is required.
|
|
435
|
+
*
|
|
436
|
+
* @return The nearest function-like scope, or the source file when no function owns it.
|
|
437
|
+
*/
|
|
438
|
+
export function declarationScope(n) {
|
|
439
|
+
const isOuterDeclarationName = (ts.isFunctionDeclaration(n.parent) || ts.isClassDeclaration(n.parent) || ts.isInterfaceDeclaration(n.parent) || ts.isTypeAliasDeclaration(n.parent)) && n.parent.name === n;
|
|
440
|
+
let scope = isOuterDeclarationName ? n.parent.parent : n.parent;
|
|
441
|
+
while (!ts.isSourceFile(scope)) {
|
|
442
|
+
if (ts.isArrowFunction(scope) ||
|
|
443
|
+
ts.isFunctionDeclaration(scope) ||
|
|
444
|
+
ts.isFunctionExpression(scope) ||
|
|
445
|
+
ts.isMethodDeclaration(scope) ||
|
|
446
|
+
ts.isConstructorDeclaration(scope) ||
|
|
447
|
+
ts.isGetAccessorDeclaration(scope) ||
|
|
448
|
+
ts.isSetAccessorDeclaration(scope))
|
|
449
|
+
return scope;
|
|
450
|
+
scope = scope.parent;
|
|
451
|
+
}
|
|
452
|
+
return scope;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Reports concise-name drift without mutating the consumer repository.
|
|
456
|
+
*
|
|
457
|
+
* @param cfg Quality configuration defining source scope and naming protections.
|
|
458
|
+
* @param analysis Optional shared program and source cache for the current quality run.
|
|
459
|
+
*
|
|
460
|
+
* @return Concise-name findings for declarations that can be renamed safely.
|
|
461
|
+
*/
|
|
462
|
+
export function namingFindings(cfg, analysis) {
|
|
463
|
+
const protectedNames = new Set([...PROTECTED_IDENTIFIERS, ...(cfg.protectedIdentifiers ?? [])]);
|
|
464
|
+
const findings = [];
|
|
465
|
+
for (const f of analysis?.files ?? srcFiles(cfg)) {
|
|
466
|
+
if (isProtectedPath(displayPath(cfg, f)))
|
|
467
|
+
continue;
|
|
468
|
+
const file = analysis?.sources.get(f) ?? ts.createSourceFile(f, readFileSync(f, 'utf8'), ts.ScriptTarget.Latest, true, f.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
469
|
+
const exportedNames = new Set(file.statements.flatMap((s) => (ts.isExportDeclaration(s) && s.exportClause && ts.isNamedExports(s.exportClause) ? s.exportClause.elements.map((e) => (e.propertyName ?? e.name).text) : [])));
|
|
470
|
+
const safetyByScope = new Map();
|
|
471
|
+
const declaredNames = new Set();
|
|
472
|
+
const collect = (n) => {
|
|
473
|
+
if (ts.isIdentifier(n) && isRenamableDeclaration(n)) {
|
|
474
|
+
const scope = declarationScope(n);
|
|
475
|
+
const safety = safetyByScope.get(scope) ?? { declared: new Set(), proposals: new Map() };
|
|
476
|
+
safety.declared.add(n.text);
|
|
477
|
+
declaredNames.add(n.text);
|
|
478
|
+
const suggestion = renameDeclaration(n);
|
|
479
|
+
if (!n.text.startsWith('_') && suggestion !== n.text && !isCallbackDeclaration(n))
|
|
480
|
+
safety.proposals.set(suggestion, (safety.proposals.get(suggestion) ?? 0) + 1);
|
|
481
|
+
safetyByScope.set(scope, safety);
|
|
482
|
+
}
|
|
483
|
+
ts.forEachChild(n, collect);
|
|
484
|
+
};
|
|
485
|
+
collect(file);
|
|
486
|
+
const inspect = (n) => {
|
|
487
|
+
if (ts.isIdentifier(n) && isRenamableDeclaration(n) && !n.text.startsWith('_') && !protectedNames.has(n.text) && !exportedNames.has(n.text)) {
|
|
488
|
+
const suggestion = renameDeclaration(n);
|
|
489
|
+
const safety = safetyByScope.get(declarationScope(n));
|
|
490
|
+
if (!isCallbackDeclaration(n) && (safety?.declared.has(suggestion) || declaredNames.has(suggestion) || (safety?.proposals.get(suggestion) ?? 0) > 1))
|
|
491
|
+
return;
|
|
492
|
+
if (suggestion !== n.text)
|
|
493
|
+
findings.push({ file: displayPath(cfg, f), name: n.text, suggestion, severity: hasPrefixReplacement(n.text) ? 'warning' : 'error', start: n.getStart(file) });
|
|
494
|
+
}
|
|
495
|
+
ts.forEachChild(n, inspect);
|
|
496
|
+
};
|
|
497
|
+
inspect(file);
|
|
498
|
+
}
|
|
499
|
+
return findings;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Checks whether a symbol name appears in another source file's text.
|
|
503
|
+
*
|
|
504
|
+
* @param files Source files to scan.
|
|
505
|
+
* @param srcTexts Cached source text keyed by absolute path.
|
|
506
|
+
* @param src File being excluded from the search.
|
|
507
|
+
* @param name Symbol name to search for as a complete word.
|
|
508
|
+
*
|
|
509
|
+
* @return `true` when another file references the name textually.
|
|
510
|
+
*/
|
|
511
|
+
function externallyReferenced(files, srcTexts, src, name) {
|
|
512
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
513
|
+
const pattern = new RegExp(`\\b${escaped}\\b`);
|
|
514
|
+
return files.some((f) => f !== src && pattern.test(srcTexts.get(f) ?? ''));
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Finds and optionally removes declarations that only rename another symbol.
|
|
518
|
+
*
|
|
519
|
+
* @param cfg Quality configuration defining the source scope and protected identifiers.
|
|
520
|
+
* @param analysisOrFix Shared analysis context, or the legacy boolean autofix flag.
|
|
521
|
+
* @param legacyFix Autofix flag used when an analysis context is supplied.
|
|
522
|
+
*
|
|
523
|
+
* @return Alias declarations that can be removed, whether or not autofix is enabled.
|
|
524
|
+
*/
|
|
525
|
+
export function aliasFindings(cfg, analysisOrFix = false, legacyFix = false) {
|
|
526
|
+
const analysis = typeof analysisOrFix === 'object' ? analysisOrFix : undefined;
|
|
527
|
+
const fix = typeof analysisOrFix === 'boolean' ? analysisOrFix : legacyFix;
|
|
528
|
+
const files = (analysis?.files ?? srcFiles(cfg)).filter((f) => /\.(?:c|m)?(?:js|ts)x?$/.test(f));
|
|
529
|
+
const srcTexts = analysis?.texts ?? new Map(files.map((f) => [f, readFileSync(f, 'utf8')]));
|
|
530
|
+
const program = analysis?.program ??
|
|
531
|
+
ts.createProgram(files, {
|
|
532
|
+
allowJs: true,
|
|
533
|
+
checkJs: false,
|
|
534
|
+
jsx: ts.JsxEmit.Preserve,
|
|
535
|
+
module: ts.ModuleKind.ESNext,
|
|
536
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
537
|
+
skipLibCheck: true,
|
|
538
|
+
target: ts.ScriptTarget.ESNext,
|
|
539
|
+
});
|
|
540
|
+
const checker = analysis?.checker ?? program.getTypeChecker();
|
|
541
|
+
const exportedSymbols = new Set();
|
|
542
|
+
const exportedNames = new Set();
|
|
543
|
+
for (const file of program.getSourceFiles()) {
|
|
544
|
+
const module = checker.getSymbolAtLocation(file);
|
|
545
|
+
if (module)
|
|
546
|
+
for (const symbol of checker.getExportsOfModule(module)) {
|
|
547
|
+
exportedNames.add(symbol.name);
|
|
548
|
+
exportedSymbols.add(symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol);
|
|
549
|
+
}
|
|
550
|
+
for (const statement of file.statements) {
|
|
551
|
+
if (!ts.isExportDeclaration(statement) || !statement.exportClause || !ts.isNamedExports(statement.exportClause))
|
|
552
|
+
continue;
|
|
553
|
+
for (const specifier of statement.exportClause.elements)
|
|
554
|
+
exportedNames.add(specifier.name.text);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
const findings = [];
|
|
558
|
+
for (const src of program.getSourceFiles()) {
|
|
559
|
+
if (!files.includes(src.fileName))
|
|
560
|
+
continue;
|
|
561
|
+
const aliases = [];
|
|
562
|
+
const inspect = (node) => {
|
|
563
|
+
if (ts.isVariableStatement(node) &&
|
|
564
|
+
(node.declarationList.flags & ts.NodeFlags.Const) !== 0 &&
|
|
565
|
+
node.declarationList.declarations.length === 1 &&
|
|
566
|
+
ts.isIdentifier(node.declarationList.declarations[0].name)) {
|
|
567
|
+
const declaration = node.declarationList.declarations[0];
|
|
568
|
+
const name = declaration.name;
|
|
569
|
+
const tgt = declaration.initializer && aliasTgt(declaration.initializer);
|
|
570
|
+
const symbol = tgt && checker.getSymbolAtLocation(name);
|
|
571
|
+
const targetSymbol = tgt
|
|
572
|
+
? (checker.getSymbolAtLocation(tgt) ??
|
|
573
|
+
(ts.isElementAccessExpression(tgt) && ts.isStringLiteral(tgt.argumentExpression)
|
|
574
|
+
? checker.getPropertyOfType(checker.getTypeAtLocation(tgt.expression), tgt.argumentExpression.text)
|
|
575
|
+
: undefined))
|
|
576
|
+
: undefined;
|
|
577
|
+
if (tgt &&
|
|
578
|
+
symbol &&
|
|
579
|
+
targetSymbol &&
|
|
580
|
+
symbol !== targetSymbol &&
|
|
581
|
+
!tgt.getText(src).includes('?.') &&
|
|
582
|
+
!exportedSymbols.has(symbol) &&
|
|
583
|
+
!exportedNames.has(name.text) &&
|
|
584
|
+
!node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword))
|
|
585
|
+
aliases.push({ statement: node, name, symbol, replacement: tgt.getText(src), kind: 'const' });
|
|
586
|
+
}
|
|
587
|
+
if (ts.isTypeAliasDeclaration(node) && node.typeParameters === undefined && ts.isTypeReferenceNode(node.type) && node.type.typeArguments === undefined) {
|
|
588
|
+
const symbol = checker.getSymbolAtLocation(node.name);
|
|
589
|
+
const targetSymbol = checker.getSymbolAtLocation(node.type.typeName);
|
|
590
|
+
if (symbol &&
|
|
591
|
+
targetSymbol &&
|
|
592
|
+
symbol !== targetSymbol &&
|
|
593
|
+
!exportedSymbols.has(symbol) &&
|
|
594
|
+
!exportedNames.has(node.name.text) &&
|
|
595
|
+
!externallyReferenced(files, srcTexts, src.fileName, node.name.text))
|
|
596
|
+
aliases.push({ statement: node, name: node.name, symbol, replacement: node.type.getText(src), kind: 'type' });
|
|
597
|
+
}
|
|
598
|
+
ts.forEachChild(node, inspect);
|
|
599
|
+
};
|
|
600
|
+
inspect(src);
|
|
601
|
+
if (!aliases.length)
|
|
602
|
+
continue;
|
|
603
|
+
const edits = [];
|
|
604
|
+
const symbols = new Map(aliases.map((a) => [a.symbol, a]));
|
|
605
|
+
const visit = (node) => {
|
|
606
|
+
if (aliases.some((a) => a.statement === node))
|
|
607
|
+
return;
|
|
608
|
+
if (ts.isIdentifier(node)) {
|
|
609
|
+
const symbol = ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node
|
|
610
|
+
? (checker.getShorthandAssignmentValueSymbol(node.parent) ?? checker.getSymbolAtLocation(node))
|
|
611
|
+
: checker.getSymbolAtLocation(node);
|
|
612
|
+
const alias = symbols.get(symbol);
|
|
613
|
+
if (alias && node !== alias.name)
|
|
614
|
+
edits.push({
|
|
615
|
+
start: node.getStart(src),
|
|
616
|
+
end: node.end,
|
|
617
|
+
text: ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node ? `${node.text}: ${alias.replacement}` : alias.replacement,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
ts.forEachChild(node, visit);
|
|
621
|
+
};
|
|
622
|
+
visit(src);
|
|
623
|
+
for (const alias of aliases) {
|
|
624
|
+
findings.push({
|
|
625
|
+
file: displayPath(cfg, src.fileName),
|
|
626
|
+
line: src.getLineAndCharacterOfPosition(alias.name.getStart(src)).line + 1,
|
|
627
|
+
kind: alias.kind,
|
|
628
|
+
name: alias.name.text,
|
|
629
|
+
replacement: alias.replacement,
|
|
630
|
+
});
|
|
631
|
+
edits.push({ start: alias.statement.getFullStart(), end: alias.statement.end, text: '' });
|
|
632
|
+
}
|
|
633
|
+
if (fix) {
|
|
634
|
+
const original = readFileSync(src.fileName, 'utf8');
|
|
635
|
+
const updated = edits.sort((a, b) => b.start - a.start).reduce((t, e) => `${t.slice(0, e.start)}${e.text}${t.slice(e.end)}`, original);
|
|
636
|
+
if (updated !== original)
|
|
637
|
+
writeFileSync(src.fileName, updated);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return findings;
|
|
641
|
+
}
|
|
642
|
+
//# sourceMappingURL=naming.js.map
|