@trebired/code-discipline 1.4.1 → 2.0.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/CHANGELOG.md +17 -0
- package/README.md +209 -170
- package/dist/checks/fix-folderization.d.ts.map +1 -1
- package/dist/checks/fix-folderization.js +3 -0
- package/dist/checks/fix-folderization.js.map +1 -1
- package/dist/checks/index.d.ts +4 -2
- package/dist/checks/index.d.ts.map +1 -1
- package/dist/checks/index.js +103 -33
- package/dist/checks/index.js.map +1 -1
- package/dist/checks/rule-slugs.d.ts +15 -0
- package/dist/checks/rule-slugs.d.ts.map +1 -0
- package/dist/checks/rule-slugs.js +62 -0
- package/dist/checks/rule-slugs.js.map +1 -0
- package/dist/checks/rules/dry.d.ts +7 -0
- package/dist/checks/rules/dry.d.ts.map +1 -0
- package/dist/checks/rules/dry.js +922 -0
- package/dist/checks/rules/dry.js.map +1 -0
- package/dist/checks/rules/max-function-lines.d.ts +6 -0
- package/dist/checks/rules/max-function-lines.d.ts.map +1 -0
- package/dist/checks/rules/max-function-lines.js +103 -0
- package/dist/checks/rules/max-function-lines.js.map +1 -0
- package/dist/checks/types.d.ts +79 -6
- package/dist/checks/types.d.ts.map +1 -1
- package/dist/cli/run-cli.d.ts.map +1 -1
- package/dist/cli/run-cli.js +22 -30
- package/dist/cli/run-cli.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -3
- package/dist/cli.js.map +1 -1
- package/dist/config/index.d.ts +4 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +144 -10
- package/dist/config/index.js.map +1 -1
- package/dist/config/normalize-check-options.d.ts +2 -2
- package/dist/config/normalize-check-options.d.ts.map +1 -1
- package/dist/config/normalize-check-options.js +7 -2
- package/dist/config/normalize-check-options.js.map +1 -1
- package/dist/config/normalize-rule-options.d.ts +13 -4
- package/dist/config/normalize-rule-options.d.ts.map +1 -1
- package/dist/config/normalize-rule-options.js +57 -1
- package/dist/config/normalize-rule-options.js.map +1 -1
- package/dist/config/normalize-sync-imports-options.d.ts.map +1 -1
- package/dist/config/normalize-sync-imports-options.js +2 -0
- package/dist/config/normalize-sync-imports-options.js.map +1 -1
- package/dist/imports/aliases.d.ts.map +1 -1
- package/dist/imports/aliases.js +6 -2
- package/dist/imports/aliases.js.map +1 -1
- package/dist/imports/check-sync-imports.d.ts.map +1 -1
- package/dist/imports/check-sync-imports.js +19 -0
- package/dist/imports/check-sync-imports.js.map +1 -1
- package/dist/imports/sync-imports.d.ts.map +1 -1
- package/dist/imports/sync-imports.js +11 -1
- package/dist/imports/sync-imports.js.map +1 -1
- package/dist/imports/types.d.ts +10 -1
- package/dist/imports/types.d.ts.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/run.d.ts +16 -19
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +28 -37
- package/dist/run.js.map +1 -1
- package/dist/runtime/orchestrate.d.ts +19 -0
- package/dist/runtime/orchestrate.d.ts.map +1 -0
- package/dist/runtime/orchestrate.js +50 -0
- package/dist/runtime/orchestrate.js.map +1 -0
- package/dist/runtime/runtime-imports-sync.d.ts +22 -0
- package/dist/runtime/runtime-imports-sync.d.ts.map +1 -0
- package/dist/runtime/runtime-imports-sync.js +89 -0
- package/dist/runtime/runtime-imports-sync.js.map +1 -0
- package/dist/runtime/tsconfig-paths.d.ts +21 -0
- package/dist/runtime/tsconfig-paths.d.ts.map +1 -0
- package/dist/runtime/tsconfig-paths.js +89 -0
- package/dist/runtime/tsconfig-paths.js.map +1 -0
- package/dist/shared/discipline-types.d.ts +1 -1
- package/dist/shared/discipline-types.d.ts.map +1 -1
- package/dist/shared/utils.d.ts +2 -1
- package/dist/shared/utils.d.ts.map +1 -1
- package/dist/shared/utils.js +14 -1
- package/dist/shared/utils.js.map +1 -1
- package/package.json +10 -2
|
@@ -0,0 +1,922 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import { parseSource } from "../../imports/module-specifiers.js";
|
|
5
|
+
import { resolveRelativeImport } from "../../imports/resolve.js";
|
|
6
|
+
import { FixFailureError, InvalidCodeDisciplineConfigError, } from "../../shared/errors.js";
|
|
7
|
+
import { isFile, stableSerialize, stripKnownExtension, toPosixPath, } from "../../shared/utils.js";
|
|
8
|
+
const DRY_RESOLUTION_EXTENSIONS = [
|
|
9
|
+
".ts",
|
|
10
|
+
".tsx",
|
|
11
|
+
".mts",
|
|
12
|
+
".js",
|
|
13
|
+
".jsx",
|
|
14
|
+
".mjs",
|
|
15
|
+
".cts",
|
|
16
|
+
".cjs",
|
|
17
|
+
];
|
|
18
|
+
const SAFE_GLOBAL_IDENTIFIERS = new Set([
|
|
19
|
+
"Array",
|
|
20
|
+
"BigInt",
|
|
21
|
+
"Boolean",
|
|
22
|
+
"Date",
|
|
23
|
+
"Error",
|
|
24
|
+
"JSON",
|
|
25
|
+
"Map",
|
|
26
|
+
"Math",
|
|
27
|
+
"Number",
|
|
28
|
+
"Object",
|
|
29
|
+
"Promise",
|
|
30
|
+
"RangeError",
|
|
31
|
+
"RegExp",
|
|
32
|
+
"Set",
|
|
33
|
+
"String",
|
|
34
|
+
"Symbol",
|
|
35
|
+
"SyntaxError",
|
|
36
|
+
"TypeError",
|
|
37
|
+
"URL",
|
|
38
|
+
"URLSearchParams",
|
|
39
|
+
"WeakMap",
|
|
40
|
+
"WeakSet",
|
|
41
|
+
"console",
|
|
42
|
+
"decodeURI",
|
|
43
|
+
"decodeURIComponent",
|
|
44
|
+
"encodeURI",
|
|
45
|
+
"encodeURIComponent",
|
|
46
|
+
"isFinite",
|
|
47
|
+
"isNaN",
|
|
48
|
+
"parseFloat",
|
|
49
|
+
"parseInt",
|
|
50
|
+
]);
|
|
51
|
+
function createSerializeContext() {
|
|
52
|
+
return {
|
|
53
|
+
nextBindingIndex: 0,
|
|
54
|
+
scopes: [],
|
|
55
|
+
usesOuterScope: false,
|
|
56
|
+
usesRestrictedRuntime: false,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function pushScope(context) {
|
|
60
|
+
context.scopes.push(new Map());
|
|
61
|
+
}
|
|
62
|
+
function popScope(context) {
|
|
63
|
+
context.scopes.pop();
|
|
64
|
+
}
|
|
65
|
+
function declareBinding(context, name) {
|
|
66
|
+
const canonical = `v${context.nextBindingIndex}`;
|
|
67
|
+
context.nextBindingIndex += 1;
|
|
68
|
+
context.scopes[context.scopes.length - 1]?.set(name, canonical);
|
|
69
|
+
return canonical;
|
|
70
|
+
}
|
|
71
|
+
function lookupBinding(context, name) {
|
|
72
|
+
for (let index = context.scopes.length - 1; index >= 0; index -= 1) {
|
|
73
|
+
const match = context.scopes[index]?.get(name);
|
|
74
|
+
if (match)
|
|
75
|
+
return match;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function isReferenceIdentifier(node) {
|
|
80
|
+
const parent = node.parent;
|
|
81
|
+
if (!parent)
|
|
82
|
+
return true;
|
|
83
|
+
if ((ts.isFunctionDeclaration(parent)
|
|
84
|
+
|| ts.isFunctionExpression(parent)
|
|
85
|
+
|| ts.isArrowFunction(parent)
|
|
86
|
+
|| ts.isMethodDeclaration(parent)
|
|
87
|
+
|| ts.isGetAccessorDeclaration(parent)
|
|
88
|
+
|| ts.isSetAccessorDeclaration(parent)
|
|
89
|
+
|| ts.isParameter(parent)
|
|
90
|
+
|| ts.isVariableDeclaration(parent)
|
|
91
|
+
|| ts.isBindingElement(parent))
|
|
92
|
+
&& parent.name === node) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if ((ts.isPropertyAssignment(parent) || ts.isPropertyDeclaration(parent) || ts.isMethodDeclaration(parent)) && parent.name === node) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
if ((ts.isPropertyAccessExpression(parent) && parent.name === node) || (ts.isQualifiedName(parent) && parent.right === node)) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
if (ts.isShorthandPropertyAssignment(parent) && parent.name === node) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if ((ts.isImportSpecifier(parent) && (parent.name === node || parent.propertyName === node))
|
|
105
|
+
|| (ts.isImportClause(parent) && parent.name === node)
|
|
106
|
+
|| (ts.isNamespaceImport(parent) && parent.name === node)
|
|
107
|
+
|| (ts.isExportSpecifier(parent) && (parent.name === node || parent.propertyName === node))) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
function serializeBindingName(name, context, sourceFile) {
|
|
113
|
+
if (ts.isIdentifier(name)) {
|
|
114
|
+
return ["id", declareBinding(context, name.text)];
|
|
115
|
+
}
|
|
116
|
+
if (ts.isObjectBindingPattern(name)) {
|
|
117
|
+
return [
|
|
118
|
+
"object-binding",
|
|
119
|
+
name.elements.map((element) => serializeBindingElement(element, context, sourceFile)),
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
return [
|
|
123
|
+
"array-binding",
|
|
124
|
+
name.elements.map((element) => (element && ts.isBindingElement(element)) ? serializeBindingElement(element, context, sourceFile) : ["hole"]),
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
function serializeBindingElement(element, context, sourceFile) {
|
|
128
|
+
return [
|
|
129
|
+
"binding-element",
|
|
130
|
+
element.dotDotDotToken ? "rest" : "value",
|
|
131
|
+
element.propertyName ? serializePropertyName(element.propertyName, context, sourceFile) : null,
|
|
132
|
+
serializeBindingName(element.name, context, sourceFile),
|
|
133
|
+
element.initializer ? serializeNode(element.initializer, context, sourceFile) : null,
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
function serializePropertyName(name, context, sourceFile) {
|
|
137
|
+
if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name)) {
|
|
138
|
+
return name.text;
|
|
139
|
+
}
|
|
140
|
+
if (ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
141
|
+
return name.text;
|
|
142
|
+
}
|
|
143
|
+
if (ts.isComputedPropertyName(name)) {
|
|
144
|
+
return ["computed", serializeNode(name.expression, context, sourceFile)];
|
|
145
|
+
}
|
|
146
|
+
return name.getText(sourceFile);
|
|
147
|
+
}
|
|
148
|
+
function serializeFunctionLike(node, context, sourceFile) {
|
|
149
|
+
const isAsync = Boolean(node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword));
|
|
150
|
+
const isGenerator = ts.isFunctionLike(node) && "asteriskToken" in node && Boolean(node.asteriskToken);
|
|
151
|
+
pushScope(context);
|
|
152
|
+
const parameters = node.parameters.map((parameter) => [
|
|
153
|
+
"param",
|
|
154
|
+
parameter.dotDotDotToken ? "rest" : "value",
|
|
155
|
+
serializeBindingName(parameter.name, context, sourceFile),
|
|
156
|
+
parameter.initializer ? serializeNode(parameter.initializer, context, sourceFile) : null,
|
|
157
|
+
]);
|
|
158
|
+
const body = node.body
|
|
159
|
+
? serializeNode(node.body, context, sourceFile)
|
|
160
|
+
: null;
|
|
161
|
+
popScope(context);
|
|
162
|
+
return [
|
|
163
|
+
"function",
|
|
164
|
+
isAsync,
|
|
165
|
+
isGenerator,
|
|
166
|
+
parameters,
|
|
167
|
+
body,
|
|
168
|
+
];
|
|
169
|
+
}
|
|
170
|
+
function serializeNode(node, context, sourceFile) {
|
|
171
|
+
if (!node)
|
|
172
|
+
return null;
|
|
173
|
+
if (ts.isTypeNode(node)
|
|
174
|
+
|| ts.isTypeParameterDeclaration(node)
|
|
175
|
+
|| ts.isTypeAliasDeclaration(node)
|
|
176
|
+
|| ts.isInterfaceDeclaration(node)
|
|
177
|
+
|| ts.isImportTypeNode(node)) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
if (ts.isBlock(node)) {
|
|
181
|
+
pushScope(context);
|
|
182
|
+
const result = ["block", node.statements.map((statement) => serializeNode(statement, context, sourceFile))];
|
|
183
|
+
popScope(context);
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
if (ts.isReturnStatement(node)) {
|
|
187
|
+
return ["return", serializeNode(node.expression, context, sourceFile)];
|
|
188
|
+
}
|
|
189
|
+
if (ts.isExpressionStatement(node)) {
|
|
190
|
+
return ["expr", serializeNode(node.expression, context, sourceFile)];
|
|
191
|
+
}
|
|
192
|
+
if (ts.isVariableStatement(node)) {
|
|
193
|
+
return ["vars", node.declarationList.flags, node.declarationList.declarations.map((declaration) => serializeNode(declaration, context, sourceFile))];
|
|
194
|
+
}
|
|
195
|
+
if (ts.isVariableDeclaration(node)) {
|
|
196
|
+
return [
|
|
197
|
+
"var",
|
|
198
|
+
serializeBindingName(node.name, context, sourceFile),
|
|
199
|
+
serializeNode(node.initializer, context, sourceFile),
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
if (ts.isIfStatement(node)) {
|
|
203
|
+
return [
|
|
204
|
+
"if",
|
|
205
|
+
serializeNode(node.expression, context, sourceFile),
|
|
206
|
+
serializeNode(node.thenStatement, context, sourceFile),
|
|
207
|
+
serializeNode(node.elseStatement, context, sourceFile),
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
if (ts.isForStatement(node)) {
|
|
211
|
+
return [
|
|
212
|
+
"for",
|
|
213
|
+
serializeNode(node.initializer, context, sourceFile),
|
|
214
|
+
serializeNode(node.condition, context, sourceFile),
|
|
215
|
+
serializeNode(node.incrementor, context, sourceFile),
|
|
216
|
+
serializeNode(node.statement, context, sourceFile),
|
|
217
|
+
];
|
|
218
|
+
}
|
|
219
|
+
if (ts.isForOfStatement(node) || ts.isForInStatement(node)) {
|
|
220
|
+
return [
|
|
221
|
+
ts.isForOfStatement(node) ? "for-of" : "for-in",
|
|
222
|
+
serializeNode(node.initializer, context, sourceFile),
|
|
223
|
+
serializeNode(node.expression, context, sourceFile),
|
|
224
|
+
serializeNode(node.statement, context, sourceFile),
|
|
225
|
+
];
|
|
226
|
+
}
|
|
227
|
+
if (ts.isWhileStatement(node) || ts.isDoStatement(node)) {
|
|
228
|
+
return [
|
|
229
|
+
ts.isWhileStatement(node) ? "while" : "do",
|
|
230
|
+
serializeNode(node.expression, context, sourceFile),
|
|
231
|
+
serializeNode(node.statement, context, sourceFile),
|
|
232
|
+
];
|
|
233
|
+
}
|
|
234
|
+
if (ts.isThrowStatement(node)) {
|
|
235
|
+
return ["throw", serializeNode(node.expression, context, sourceFile)];
|
|
236
|
+
}
|
|
237
|
+
if (ts.isTryStatement(node)) {
|
|
238
|
+
return [
|
|
239
|
+
"try",
|
|
240
|
+
serializeNode(node.tryBlock, context, sourceFile),
|
|
241
|
+
node.catchClause
|
|
242
|
+
? [
|
|
243
|
+
"catch",
|
|
244
|
+
node.catchClause.variableDeclaration
|
|
245
|
+
? serializeBindingName(node.catchClause.variableDeclaration.name, context, sourceFile)
|
|
246
|
+
: null,
|
|
247
|
+
serializeNode(node.catchClause.block, context, sourceFile),
|
|
248
|
+
]
|
|
249
|
+
: null,
|
|
250
|
+
serializeNode(node.finallyBlock, context, sourceFile),
|
|
251
|
+
];
|
|
252
|
+
}
|
|
253
|
+
if (ts.isSwitchStatement(node)) {
|
|
254
|
+
return [
|
|
255
|
+
"switch",
|
|
256
|
+
serializeNode(node.expression, context, sourceFile),
|
|
257
|
+
node.caseBlock.clauses.map((clause) => (ts.isCaseClause(clause)
|
|
258
|
+
? ["case", serializeNode(clause.expression, context, sourceFile), clause.statements.map((statement) => serializeNode(statement, context, sourceFile))]
|
|
259
|
+
: ["default", clause.statements.map((statement) => serializeNode(statement, context, sourceFile))])),
|
|
260
|
+
];
|
|
261
|
+
}
|
|
262
|
+
if (ts.isParenthesizedExpression(node)) {
|
|
263
|
+
return serializeNode(node.expression, context, sourceFile);
|
|
264
|
+
}
|
|
265
|
+
if (ts.isIdentifier(node)) {
|
|
266
|
+
if (node.text === "arguments") {
|
|
267
|
+
context.usesRestrictedRuntime = true;
|
|
268
|
+
}
|
|
269
|
+
if (!isReferenceIdentifier(node)) {
|
|
270
|
+
return node.text;
|
|
271
|
+
}
|
|
272
|
+
const local = lookupBinding(context, node.text);
|
|
273
|
+
if (local) {
|
|
274
|
+
return ["local", local];
|
|
275
|
+
}
|
|
276
|
+
if (node.text === "arguments") {
|
|
277
|
+
return ["restricted", "arguments"];
|
|
278
|
+
}
|
|
279
|
+
if (SAFE_GLOBAL_IDENTIFIERS.has(node.text)) {
|
|
280
|
+
return ["global", node.text];
|
|
281
|
+
}
|
|
282
|
+
context.usesOuterScope = true;
|
|
283
|
+
return ["outer", node.text];
|
|
284
|
+
}
|
|
285
|
+
if (node.kind === ts.SyntaxKind.ThisKeyword || node.kind === ts.SyntaxKind.SuperKeyword) {
|
|
286
|
+
context.usesRestrictedRuntime = true;
|
|
287
|
+
return ["restricted", ts.SyntaxKind[node.kind]];
|
|
288
|
+
}
|
|
289
|
+
if (ts.isMetaProperty(node) && node.keywordToken === ts.SyntaxKind.NewKeyword && node.name.text === "target") {
|
|
290
|
+
context.usesRestrictedRuntime = true;
|
|
291
|
+
return ["restricted", "new.target"];
|
|
292
|
+
}
|
|
293
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isNumericLiteral(node)) {
|
|
294
|
+
return ["literal", node.text];
|
|
295
|
+
}
|
|
296
|
+
if (ts.isBigIntLiteral(node)) {
|
|
297
|
+
return ["literal", node.text];
|
|
298
|
+
}
|
|
299
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword) {
|
|
300
|
+
return ["literal", ts.tokenToString(node.kind) ?? ts.SyntaxKind[node.kind]];
|
|
301
|
+
}
|
|
302
|
+
if (ts.isTemplateExpression(node)) {
|
|
303
|
+
return [
|
|
304
|
+
"template",
|
|
305
|
+
node.head.text,
|
|
306
|
+
node.templateSpans.map((span) => [
|
|
307
|
+
serializeNode(span.expression, context, sourceFile),
|
|
308
|
+
span.literal.text,
|
|
309
|
+
]),
|
|
310
|
+
];
|
|
311
|
+
}
|
|
312
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
313
|
+
return [
|
|
314
|
+
"array",
|
|
315
|
+
node.elements.map((element) => (ts.isSpreadElement(element)
|
|
316
|
+
? ["spread", serializeNode(element.expression, context, sourceFile)]
|
|
317
|
+
: serializeNode(element, context, sourceFile))),
|
|
318
|
+
];
|
|
319
|
+
}
|
|
320
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
321
|
+
return [
|
|
322
|
+
"object",
|
|
323
|
+
node.properties.map((property) => {
|
|
324
|
+
if (ts.isPropertyAssignment(property)) {
|
|
325
|
+
return ["prop", serializePropertyName(property.name, context, sourceFile), serializeNode(property.initializer, context, sourceFile)];
|
|
326
|
+
}
|
|
327
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
328
|
+
return [
|
|
329
|
+
"shorthand",
|
|
330
|
+
property.name.text,
|
|
331
|
+
serializeNode(property.name, context, sourceFile),
|
|
332
|
+
];
|
|
333
|
+
}
|
|
334
|
+
if (ts.isSpreadAssignment(property)) {
|
|
335
|
+
return ["spread-assignment", serializeNode(property.expression, context, sourceFile)];
|
|
336
|
+
}
|
|
337
|
+
if (ts.isMethodDeclaration(property)) {
|
|
338
|
+
return ["method", serializePropertyName(property.name, context, sourceFile), serializeFunctionLike(property, context, sourceFile)];
|
|
339
|
+
}
|
|
340
|
+
return ["property", property.getText(sourceFile)];
|
|
341
|
+
}),
|
|
342
|
+
];
|
|
343
|
+
}
|
|
344
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
345
|
+
return ["property-access", serializeNode(node.expression, context, sourceFile), node.name.text];
|
|
346
|
+
}
|
|
347
|
+
if (ts.isElementAccessExpression(node)) {
|
|
348
|
+
return ["element-access", serializeNode(node.expression, context, sourceFile), serializeNode(node.argumentExpression, context, sourceFile)];
|
|
349
|
+
}
|
|
350
|
+
if (ts.isCallExpression(node)) {
|
|
351
|
+
return [
|
|
352
|
+
"call",
|
|
353
|
+
serializeNode(node.expression, context, sourceFile),
|
|
354
|
+
node.arguments.map((argument) => serializeNode(argument, context, sourceFile)),
|
|
355
|
+
];
|
|
356
|
+
}
|
|
357
|
+
if (ts.isNewExpression(node)) {
|
|
358
|
+
return [
|
|
359
|
+
"new",
|
|
360
|
+
serializeNode(node.expression, context, sourceFile),
|
|
361
|
+
(node.arguments ?? []).map((argument) => serializeNode(argument, context, sourceFile)),
|
|
362
|
+
];
|
|
363
|
+
}
|
|
364
|
+
if (ts.isAwaitExpression(node)) {
|
|
365
|
+
return ["await", serializeNode(node.expression, context, sourceFile)];
|
|
366
|
+
}
|
|
367
|
+
if (ts.isYieldExpression(node)) {
|
|
368
|
+
return ["yield", node.asteriskToken ? "*" : "", serializeNode(node.expression, context, sourceFile)];
|
|
369
|
+
}
|
|
370
|
+
if (ts.isConditionalExpression(node)) {
|
|
371
|
+
return [
|
|
372
|
+
"conditional",
|
|
373
|
+
serializeNode(node.condition, context, sourceFile),
|
|
374
|
+
serializeNode(node.whenTrue, context, sourceFile),
|
|
375
|
+
serializeNode(node.whenFalse, context, sourceFile),
|
|
376
|
+
];
|
|
377
|
+
}
|
|
378
|
+
if (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) {
|
|
379
|
+
return [
|
|
380
|
+
ts.isPrefixUnaryExpression(node) ? "prefix" : "postfix",
|
|
381
|
+
ts.tokenToString(node.operator) ?? String(node.operator),
|
|
382
|
+
serializeNode(node.operand, context, sourceFile),
|
|
383
|
+
];
|
|
384
|
+
}
|
|
385
|
+
if (ts.isBinaryExpression(node)) {
|
|
386
|
+
return [
|
|
387
|
+
"binary",
|
|
388
|
+
ts.tokenToString(node.operatorToken.kind) ?? String(node.operatorToken.kind),
|
|
389
|
+
serializeNode(node.left, context, sourceFile),
|
|
390
|
+
serializeNode(node.right, context, sourceFile),
|
|
391
|
+
];
|
|
392
|
+
}
|
|
393
|
+
if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node)) {
|
|
394
|
+
return serializeFunctionLike(node, context, sourceFile);
|
|
395
|
+
}
|
|
396
|
+
if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node) || ts.isSatisfiesExpression(node)) {
|
|
397
|
+
return serializeNode(node.expression, context, sourceFile);
|
|
398
|
+
}
|
|
399
|
+
if (ts.isClassExpression(node) || ts.isClassDeclaration(node)) {
|
|
400
|
+
return ["class", node.name?.text ?? null];
|
|
401
|
+
}
|
|
402
|
+
return [
|
|
403
|
+
ts.SyntaxKind[node.kind],
|
|
404
|
+
node.getChildren(sourceFile).map((child) => serializeNode(child, context, sourceFile)),
|
|
405
|
+
];
|
|
406
|
+
}
|
|
407
|
+
function createFunctionFingerprint(node, sourceFile) {
|
|
408
|
+
const context = createSerializeContext();
|
|
409
|
+
const normalized = serializeFunctionLike(node, context, sourceFile);
|
|
410
|
+
return {
|
|
411
|
+
fingerprint: stableSerialize(normalized),
|
|
412
|
+
usesOuterScope: context.usesOuterScope,
|
|
413
|
+
usesRestrictedRuntime: context.usesRestrictedRuntime,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function resolveStandaloneName(node) {
|
|
417
|
+
if ((ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)) && node.name) {
|
|
418
|
+
return node.name.text;
|
|
419
|
+
}
|
|
420
|
+
const parent = node.parent;
|
|
421
|
+
if (parent && ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
|
|
422
|
+
return parent.name.text;
|
|
423
|
+
}
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
function resolveClassification(node) {
|
|
427
|
+
if (ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node) || ts.isConstructorDeclaration(node)) {
|
|
428
|
+
return "method";
|
|
429
|
+
}
|
|
430
|
+
const parent = node.parent;
|
|
431
|
+
if (parent && ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
|
|
432
|
+
return "standalone";
|
|
433
|
+
}
|
|
434
|
+
if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node)) {
|
|
435
|
+
return resolveStandaloneName(node) ? "standalone" : "unsupported";
|
|
436
|
+
}
|
|
437
|
+
return "unsupported";
|
|
438
|
+
}
|
|
439
|
+
function resolveRemovalRange(node, sourceFile) {
|
|
440
|
+
if (ts.isFunctionDeclaration(node) && node.parent) {
|
|
441
|
+
return {
|
|
442
|
+
start: node.getFullStart(),
|
|
443
|
+
end: expandRemovalEnd(sourceFile.text, node.getEnd()),
|
|
444
|
+
ok: true,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
const parent = node.parent;
|
|
448
|
+
if (parent && ts.isVariableDeclaration(parent)) {
|
|
449
|
+
const declarationList = parent.parent;
|
|
450
|
+
const statement = declarationList?.parent;
|
|
451
|
+
const isSingleDeclaration = ts.isVariableDeclarationList(declarationList)
|
|
452
|
+
&& declarationList.declarations.length === 1
|
|
453
|
+
&& ts.isVariableStatement(statement);
|
|
454
|
+
if (!isSingleDeclaration) {
|
|
455
|
+
return {
|
|
456
|
+
start: parent.getStart(sourceFile),
|
|
457
|
+
end: parent.getEnd(),
|
|
458
|
+
ok: false,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
start: statement.getFullStart(),
|
|
463
|
+
end: expandRemovalEnd(sourceFile.text, statement.getEnd()),
|
|
464
|
+
ok: true,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
start: node.getStart(sourceFile),
|
|
469
|
+
end: node.getEnd(),
|
|
470
|
+
ok: false,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function expandRemovalEnd(text, end) {
|
|
474
|
+
if (text.slice(end, end + 2) === "\r\n")
|
|
475
|
+
return end + 2;
|
|
476
|
+
if (text[end] === "\n")
|
|
477
|
+
return end + 1;
|
|
478
|
+
return end;
|
|
479
|
+
}
|
|
480
|
+
async function resolveDryHelperModulePath(reference, options) {
|
|
481
|
+
const baseDir = options.configPath ? path.dirname(options.configPath) : options.projectRoot;
|
|
482
|
+
const basePath = path.isAbsolute(reference.from)
|
|
483
|
+
? path.resolve(reference.from)
|
|
484
|
+
: path.resolve(baseDir, reference.from);
|
|
485
|
+
const candidates = [basePath];
|
|
486
|
+
if (!path.extname(basePath)) {
|
|
487
|
+
for (const extension of DRY_RESOLUTION_EXTENSIONS) {
|
|
488
|
+
candidates.push(`${basePath}${extension}`);
|
|
489
|
+
}
|
|
490
|
+
for (const extension of DRY_RESOLUTION_EXTENSIONS) {
|
|
491
|
+
candidates.push(path.join(basePath, `index${extension}`));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
for (const candidate of candidates) {
|
|
495
|
+
if (await isFile(candidate)) {
|
|
496
|
+
return candidate;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
throw new InvalidCodeDisciplineConfigError(`dry helper module was not found: ${reference.from}`, {
|
|
500
|
+
rule: "dry",
|
|
501
|
+
from: reference.from,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
function extractSupportedExportedFunction(sourceFile, exportName) {
|
|
505
|
+
for (const statement of sourceFile.statements) {
|
|
506
|
+
if (exportName === "default" && ts.isExportAssignment(statement)) {
|
|
507
|
+
const expression = statement.expression;
|
|
508
|
+
if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
|
|
509
|
+
return expression;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (ts.isFunctionDeclaration(statement) && statement.name?.text === exportName && statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
513
|
+
return statement;
|
|
514
|
+
}
|
|
515
|
+
if (ts.isVariableStatement(statement) && statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
516
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
517
|
+
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== exportName || !declaration.initializer)
|
|
518
|
+
continue;
|
|
519
|
+
if (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer)) {
|
|
520
|
+
return declaration.initializer;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (exportName === "default" && ts.isFunctionDeclaration(statement) && statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword)) {
|
|
525
|
+
return statement;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
async function resolveDryHelpers(rule, options) {
|
|
531
|
+
const helpers = new Map();
|
|
532
|
+
for (const reference of rule.helpers) {
|
|
533
|
+
const absolutePath = await resolveDryHelperModulePath(reference, options);
|
|
534
|
+
const sourceText = await fs.readFile(absolutePath, "utf8");
|
|
535
|
+
const sourceFile = parseSource(sourceText, absolutePath);
|
|
536
|
+
const exportedFunction = extractSupportedExportedFunction(sourceFile, reference.exportName);
|
|
537
|
+
if (!exportedFunction) {
|
|
538
|
+
throw new InvalidCodeDisciplineConfigError(`dry helper export is not a supported function: ${reference.from}#${reference.exportName}`, {
|
|
539
|
+
rule: "dry",
|
|
540
|
+
exportName: reference.exportName,
|
|
541
|
+
from: reference.from,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
const fingerprintState = createFunctionFingerprint(exportedFunction, sourceFile);
|
|
545
|
+
const helperKey = reference.key || `${reference.from}#${reference.exportName}`;
|
|
546
|
+
const localName = reference.exportName === "default"
|
|
547
|
+
? resolveStandaloneName(exportedFunction) || "default"
|
|
548
|
+
: reference.exportName;
|
|
549
|
+
if (helpers.has(fingerprintState.fingerprint)) {
|
|
550
|
+
const existing = helpers.get(fingerprintState.fingerprint);
|
|
551
|
+
throw new InvalidCodeDisciplineConfigError(`dry helper fingerprint collision: ${helperKey} conflicts with ${existing.helperKey}`, {
|
|
552
|
+
rule: "dry",
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
helpers.set(fingerprintState.fingerprint, {
|
|
556
|
+
absolutePath,
|
|
557
|
+
exportName: reference.exportName,
|
|
558
|
+
fingerprint: fingerprintState.fingerprint,
|
|
559
|
+
filePath: toPosixPath(path.relative(options.projectRoot, absolutePath)),
|
|
560
|
+
helperKey,
|
|
561
|
+
importPath: reference.from,
|
|
562
|
+
localName,
|
|
563
|
+
nodeEnd: exportedFunction.getEnd(),
|
|
564
|
+
nodeStart: exportedFunction.getStart(sourceFile),
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
return helpers;
|
|
568
|
+
}
|
|
569
|
+
function createDryViolation(candidate, options) {
|
|
570
|
+
return {
|
|
571
|
+
rule: "dry",
|
|
572
|
+
severity: options.rules.dry?.severity ?? "error",
|
|
573
|
+
fix: options.rules.dry?.fix ?? false,
|
|
574
|
+
filePath: candidate.filePath,
|
|
575
|
+
message: `${candidate.localName ?? "anonymous function"} duplicates registered helper ${candidate.helper.helperKey}`,
|
|
576
|
+
details: {
|
|
577
|
+
fixable: candidate.safeToFix,
|
|
578
|
+
helper: candidate.helper.helperKey,
|
|
579
|
+
helperFile: candidate.helper.filePath,
|
|
580
|
+
reason: candidate.nonFixableReason,
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
async function collectDryCandidates(sourceFiles, helpers, options) {
|
|
585
|
+
const results = [];
|
|
586
|
+
for (const file of sourceFiles) {
|
|
587
|
+
const text = await fs.readFile(file.absolutePath, "utf8");
|
|
588
|
+
const sourceFile = parseSource(text, file.absolutePath);
|
|
589
|
+
function visit(node) {
|
|
590
|
+
if ((ts.isFunctionDeclaration(node) && node.body)
|
|
591
|
+
|| (ts.isFunctionExpression(node) && node.body)
|
|
592
|
+
|| (ts.isArrowFunction(node) && node.body)
|
|
593
|
+
|| (ts.isMethodDeclaration(node) && node.body)
|
|
594
|
+
|| (ts.isGetAccessorDeclaration(node) && node.body)
|
|
595
|
+
|| (ts.isSetAccessorDeclaration(node) && node.body)
|
|
596
|
+
|| (ts.isConstructorDeclaration(node) && node.body)) {
|
|
597
|
+
const classification = resolveClassification(node);
|
|
598
|
+
const localName = resolveStandaloneName(node);
|
|
599
|
+
const removal = resolveRemovalRange(node, sourceFile);
|
|
600
|
+
const fingerprintState = createFunctionFingerprint(node, sourceFile);
|
|
601
|
+
const helper = helpers.get(fingerprintState.fingerprint);
|
|
602
|
+
if (helper) {
|
|
603
|
+
const isSelf = helper.absolutePath === file.absolutePath
|
|
604
|
+
&& helper.nodeStart === node.getStart(sourceFile)
|
|
605
|
+
&& helper.nodeEnd === node.getEnd();
|
|
606
|
+
if (!isSelf) {
|
|
607
|
+
const safeToFix = classification === "standalone"
|
|
608
|
+
&& removal.ok
|
|
609
|
+
&& !fingerprintState.usesOuterScope
|
|
610
|
+
&& !fingerprintState.usesRestrictedRuntime
|
|
611
|
+
&& Boolean(localName);
|
|
612
|
+
let nonFixableReason;
|
|
613
|
+
if (classification === "method") {
|
|
614
|
+
nonFixableReason = "methods are report-only in v1";
|
|
615
|
+
}
|
|
616
|
+
else if (classification !== "standalone") {
|
|
617
|
+
nonFixableReason = "unsupported function shape";
|
|
618
|
+
}
|
|
619
|
+
else if (!removal.ok) {
|
|
620
|
+
nonFixableReason = "duplicate declaration cannot be removed safely";
|
|
621
|
+
}
|
|
622
|
+
else if (fingerprintState.usesOuterScope) {
|
|
623
|
+
nonFixableReason = "duplicate captures outer scope";
|
|
624
|
+
}
|
|
625
|
+
else if (fingerprintState.usesRestrictedRuntime) {
|
|
626
|
+
nonFixableReason = "duplicate depends on this, super, arguments, or new.target";
|
|
627
|
+
}
|
|
628
|
+
results.push({
|
|
629
|
+
absolutePath: file.absolutePath,
|
|
630
|
+
classification,
|
|
631
|
+
fingerprint: fingerprintState.fingerprint,
|
|
632
|
+
filePath: file.relativeFromProjectRoot,
|
|
633
|
+
helper,
|
|
634
|
+
localName,
|
|
635
|
+
nonFixableReason,
|
|
636
|
+
removalEnd: removal.end,
|
|
637
|
+
removalStart: removal.start,
|
|
638
|
+
safeToFix,
|
|
639
|
+
sourceFile,
|
|
640
|
+
usesOuterScope: fingerprintState.usesOuterScope,
|
|
641
|
+
usesRestrictedRuntime: fingerprintState.usesRestrictedRuntime,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
ts.forEachChild(node, visit);
|
|
647
|
+
}
|
|
648
|
+
visit(sourceFile);
|
|
649
|
+
}
|
|
650
|
+
return results;
|
|
651
|
+
}
|
|
652
|
+
function formatRelativeImport(fromAbsolutePath, toAbsolutePath, extensions) {
|
|
653
|
+
let relativePath = toPosixPath(path.relative(path.dirname(fromAbsolutePath), toAbsolutePath));
|
|
654
|
+
if (!relativePath.startsWith(".")) {
|
|
655
|
+
relativePath = `./${relativePath}`;
|
|
656
|
+
}
|
|
657
|
+
const withoutExtension = stripKnownExtension(relativePath, extensions);
|
|
658
|
+
return withoutExtension.replace(/\/index$/, "") || ".";
|
|
659
|
+
}
|
|
660
|
+
async function collectExistingImports(sourceFile, filePath, options) {
|
|
661
|
+
const bindingsByTarget = new Map();
|
|
662
|
+
for (const statement of sourceFile.statements) {
|
|
663
|
+
if (!ts.isImportDeclaration(statement) || !statement.moduleSpecifier || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
const resolved = await resolveRelativeImport(statement.moduleSpecifier.text, filePath, {
|
|
667
|
+
projectRoot: options.projectRoot,
|
|
668
|
+
sourceRoot: options.sourceRoot,
|
|
669
|
+
sourceRootRelative: options.sourceRootRelative,
|
|
670
|
+
sourceExtensions: options.sourceExtensions,
|
|
671
|
+
excludeDirs: options.excludeDirs,
|
|
672
|
+
});
|
|
673
|
+
if (!resolved)
|
|
674
|
+
continue;
|
|
675
|
+
const bindings = bindingsByTarget.get(resolved) ?? [];
|
|
676
|
+
const clause = statement.importClause;
|
|
677
|
+
if (!clause)
|
|
678
|
+
continue;
|
|
679
|
+
if (clause.name) {
|
|
680
|
+
bindings.push({
|
|
681
|
+
exportName: "default",
|
|
682
|
+
localName: clause.name.text,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
|
686
|
+
for (const element of clause.namedBindings.elements) {
|
|
687
|
+
bindings.push({
|
|
688
|
+
exportName: element.propertyName?.text ?? element.name.text,
|
|
689
|
+
localName: element.name.text,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
bindingsByTarget.set(resolved, bindings);
|
|
694
|
+
}
|
|
695
|
+
return bindingsByTarget;
|
|
696
|
+
}
|
|
697
|
+
function collectTopLevelValueBindings(sourceFile) {
|
|
698
|
+
const names = new Set();
|
|
699
|
+
for (const statement of sourceFile.statements) {
|
|
700
|
+
if (ts.isFunctionDeclaration(statement) && statement.name) {
|
|
701
|
+
names.add(statement.name.text);
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
if (ts.isClassDeclaration(statement) && statement.name) {
|
|
705
|
+
names.add(statement.name.text);
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
if (ts.isImportDeclaration(statement) && statement.importClause) {
|
|
709
|
+
if (statement.importClause.name) {
|
|
710
|
+
names.add(statement.importClause.name.text);
|
|
711
|
+
}
|
|
712
|
+
if (statement.importClause.namedBindings) {
|
|
713
|
+
if (ts.isNamespaceImport(statement.importClause.namedBindings)) {
|
|
714
|
+
names.add(statement.importClause.namedBindings.name.text);
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
for (const element of statement.importClause.namedBindings.elements) {
|
|
718
|
+
names.add(element.name.text);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
if (ts.isVariableStatement(statement)) {
|
|
725
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
726
|
+
collectBoundNamesFromBindingName(declaration.name, names);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return names;
|
|
731
|
+
}
|
|
732
|
+
function collectBoundNamesFromBindingName(name, target) {
|
|
733
|
+
if (ts.isIdentifier(name)) {
|
|
734
|
+
target.add(name.text);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
|
|
738
|
+
for (const element of name.elements) {
|
|
739
|
+
if (!element || !ts.isBindingElement(element))
|
|
740
|
+
continue;
|
|
741
|
+
collectBoundNamesFromBindingName(element.name, target);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
function findImportInsertionOffset(sourceFile, text) {
|
|
746
|
+
const imports = sourceFile.statements.filter((statement) => ts.isImportDeclaration(statement));
|
|
747
|
+
if (imports.length > 0) {
|
|
748
|
+
return imports[imports.length - 1].getEnd();
|
|
749
|
+
}
|
|
750
|
+
let offset = 0;
|
|
751
|
+
if (text.startsWith("#!")) {
|
|
752
|
+
const newlineIndex = text.indexOf("\n");
|
|
753
|
+
offset = newlineIndex >= 0 ? newlineIndex + 1 : text.length;
|
|
754
|
+
}
|
|
755
|
+
for (const statement of sourceFile.statements) {
|
|
756
|
+
if (ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression)) {
|
|
757
|
+
offset = statement.getEnd();
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
return offset;
|
|
763
|
+
}
|
|
764
|
+
async function fixDryRule(sourceFiles, options) {
|
|
765
|
+
const rule = options.rules.dry;
|
|
766
|
+
if (!rule) {
|
|
767
|
+
return {
|
|
768
|
+
ok: true,
|
|
769
|
+
errors: 0,
|
|
770
|
+
warnings: 0,
|
|
771
|
+
violations: [],
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
const helpers = await resolveDryHelpers(rule, options);
|
|
775
|
+
const candidates = await collectDryCandidates(sourceFiles, helpers, options);
|
|
776
|
+
const violations = candidates.map((candidate) => createDryViolation(candidate, options));
|
|
777
|
+
const warnings = violations.filter((violation) => violation.severity === "warning").length;
|
|
778
|
+
const errors = violations.length - warnings;
|
|
779
|
+
if (violations.length === 0) {
|
|
780
|
+
return {
|
|
781
|
+
ok: true,
|
|
782
|
+
errors: 0,
|
|
783
|
+
warnings: 0,
|
|
784
|
+
violations: [],
|
|
785
|
+
added_imports: 0,
|
|
786
|
+
removed_duplicates: 0,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
if (!rule.fix) {
|
|
790
|
+
return {
|
|
791
|
+
ok: errors === 0,
|
|
792
|
+
errors,
|
|
793
|
+
warnings,
|
|
794
|
+
violations,
|
|
795
|
+
added_imports: 0,
|
|
796
|
+
removed_duplicates: 0,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
const candidatesByFile = new Map();
|
|
800
|
+
const fixedCandidates = new Set();
|
|
801
|
+
for (const candidate of candidates.filter((entry) => entry.safeToFix)) {
|
|
802
|
+
const rows = candidatesByFile.get(candidate.absolutePath) ?? [];
|
|
803
|
+
rows.push(candidate);
|
|
804
|
+
candidatesByFile.set(candidate.absolutePath, rows);
|
|
805
|
+
}
|
|
806
|
+
let addedImports = 0;
|
|
807
|
+
let removedDuplicates = 0;
|
|
808
|
+
let rewrittenFiles = 0;
|
|
809
|
+
try {
|
|
810
|
+
for (const [absolutePath, fileCandidates] of candidatesByFile) {
|
|
811
|
+
const originalText = await fs.readFile(absolutePath, "utf8");
|
|
812
|
+
const sourceFile = fileCandidates[0].sourceFile;
|
|
813
|
+
const existingImports = await collectExistingImports(sourceFile, absolutePath, options);
|
|
814
|
+
const topLevelBindings = collectTopLevelValueBindings(sourceFile);
|
|
815
|
+
const pendingImports = new Map();
|
|
816
|
+
const removals = [];
|
|
817
|
+
for (const statement of sourceFile.statements) {
|
|
818
|
+
if (ts.isFunctionDeclaration(statement) && statement.name) {
|
|
819
|
+
const matchingCandidate = fileCandidates.find((candidate) => candidate.localName === statement.name?.text && candidate.removalStart === statement.getFullStart());
|
|
820
|
+
if (matchingCandidate) {
|
|
821
|
+
topLevelBindings.delete(statement.name.text);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (ts.isVariableStatement(statement) && statement.declarationList.declarations.length === 1) {
|
|
825
|
+
const declaration = statement.declarationList.declarations[0];
|
|
826
|
+
if (!ts.isIdentifier(declaration.name))
|
|
827
|
+
continue;
|
|
828
|
+
const variableName = declaration.name.text;
|
|
829
|
+
const matchingCandidate = fileCandidates.find((candidate) => candidate.localName === variableName && candidate.removalStart === statement.getFullStart());
|
|
830
|
+
if (matchingCandidate) {
|
|
831
|
+
topLevelBindings.delete(variableName);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
for (const candidate of fileCandidates.sort((left, right) => right.removalStart - left.removalStart)) {
|
|
836
|
+
if (!candidate.localName)
|
|
837
|
+
continue;
|
|
838
|
+
if (candidate.helper.absolutePath === absolutePath) {
|
|
839
|
+
if (candidate.localName !== candidate.helper.localName) {
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
removals.push({
|
|
843
|
+
start: candidate.removalStart,
|
|
844
|
+
end: candidate.removalEnd,
|
|
845
|
+
});
|
|
846
|
+
fixedCandidates.add(candidate);
|
|
847
|
+
removedDuplicates += 1;
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
const existingHelperBindings = existingImports.get(candidate.helper.absolutePath) ?? [];
|
|
851
|
+
const alreadyImported = existingHelperBindings.some((binding) => binding.exportName === candidate.helper.exportName && binding.localName === candidate.localName);
|
|
852
|
+
if (!alreadyImported) {
|
|
853
|
+
if (topLevelBindings.has(candidate.localName)) {
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
const importSpecifier = formatRelativeImport(absolutePath, candidate.helper.absolutePath, options.sourceExtensions);
|
|
857
|
+
const importLine = candidate.helper.exportName === "default"
|
|
858
|
+
? `import ${candidate.localName} from "${importSpecifier}";\n`
|
|
859
|
+
: candidate.localName === candidate.helper.exportName
|
|
860
|
+
? `import { ${candidate.helper.exportName} } from "${importSpecifier}";\n`
|
|
861
|
+
: `import { ${candidate.helper.exportName} as ${candidate.localName} } from "${importSpecifier}";\n`;
|
|
862
|
+
pendingImports.set(`${candidate.helper.absolutePath}::${candidate.localName}`, importLine);
|
|
863
|
+
topLevelBindings.add(candidate.localName);
|
|
864
|
+
}
|
|
865
|
+
removals.push({
|
|
866
|
+
start: candidate.removalStart,
|
|
867
|
+
end: candidate.removalEnd,
|
|
868
|
+
});
|
|
869
|
+
fixedCandidates.add(candidate);
|
|
870
|
+
removedDuplicates += 1;
|
|
871
|
+
}
|
|
872
|
+
if (removals.length === 0 && pendingImports.size === 0) {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
let nextText = originalText;
|
|
876
|
+
for (const removal of removals.sort((left, right) => right.start - left.start)) {
|
|
877
|
+
nextText = `${nextText.slice(0, removal.start)}${nextText.slice(removal.end)}`;
|
|
878
|
+
}
|
|
879
|
+
if (pendingImports.size > 0) {
|
|
880
|
+
const insertionOffset = findImportInsertionOffset(sourceFile, originalText);
|
|
881
|
+
const prefix = nextText.slice(0, insertionOffset);
|
|
882
|
+
const suffix = nextText.slice(insertionOffset);
|
|
883
|
+
const importBlock = `${prefix.endsWith("\n") || insertionOffset === 0 ? "" : "\n"}${[...pendingImports.values()].join("")}`;
|
|
884
|
+
nextText = `${prefix}${importBlock}${suffix.startsWith("\n") ? "" : "\n"}${suffix}`;
|
|
885
|
+
addedImports += pendingImports.size;
|
|
886
|
+
}
|
|
887
|
+
if (nextText !== originalText) {
|
|
888
|
+
await fs.writeFile(absolutePath, nextText);
|
|
889
|
+
rewrittenFiles += 1;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
catch (error) {
|
|
894
|
+
throw new FixFailureError("DRY fix failed", {
|
|
895
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
const remainingViolations = candidates
|
|
899
|
+
.filter((candidate) => !fixedCandidates.has(candidate))
|
|
900
|
+
.map((candidate) => createDryViolation(candidate, options));
|
|
901
|
+
const remainingWarnings = remainingViolations.filter((violation) => violation.severity === "warning").length;
|
|
902
|
+
const remainingErrors = remainingViolations.length - remainingWarnings;
|
|
903
|
+
return {
|
|
904
|
+
ok: remainingErrors === 0,
|
|
905
|
+
errors: remainingErrors,
|
|
906
|
+
warnings: remainingWarnings,
|
|
907
|
+
violations: remainingViolations,
|
|
908
|
+
added_imports: addedImports,
|
|
909
|
+
removed_duplicates: removedDuplicates,
|
|
910
|
+
rewritten_files: rewrittenFiles,
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
async function collectDryViolations(sourceFiles, options) {
|
|
914
|
+
const rule = options.rules.dry;
|
|
915
|
+
if (!rule)
|
|
916
|
+
return [];
|
|
917
|
+
const helpers = await resolveDryHelpers(rule, options);
|
|
918
|
+
const candidates = await collectDryCandidates(sourceFiles, helpers, options);
|
|
919
|
+
return candidates.map((candidate) => createDryViolation(candidate, options));
|
|
920
|
+
}
|
|
921
|
+
export { collectDryViolations, fixDryRule };
|
|
922
|
+
//# sourceMappingURL=dry.js.map
|