@aiready/consistency 0.16.3 → 0.16.5
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/.turbo/turbo-build.log +8 -8
- package/.turbo/turbo-test.log +9 -11
- package/dist/chunk-IXBC6GVT.mjs +832 -0
- package/dist/cli.js +12 -1
- package/dist/cli.mjs +1 -1
- package/dist/index.js +21 -4
- package/dist/index.mjs +10 -4
- package/package.json +2 -2
- package/src/analyzer.ts +29 -15
- package/src/analyzers/naming-ast.ts +18 -2
- package/src/analyzers/naming.ts +15 -6
- package/src/analyzers/patterns.ts +20 -10
- package/src/types.ts +7 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @aiready/consistency@0.16.
|
|
3
|
+
> @aiready/consistency@0.16.4 build /Users/pengcao/projects/aiready/packages/consistency
|
|
4
4
|
> tsup src/index.ts src/cli.ts --format cjs,esm --dts
|
|
5
5
|
|
|
6
6
|
[34mCLI[39m Building entry: src/cli.ts, src/index.ts
|
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
[34mCLI[39m Target: es2020
|
|
10
10
|
[34mCJS[39m Build start
|
|
11
11
|
[34mESM[39m Build start
|
|
12
|
-
[32mCJS[39m [1mdist/cli.js [22m[32m35.77 KB[39m
|
|
13
|
-
[32mCJS[39m [1mdist/index.js [22m[32m33.20 KB[39m
|
|
14
|
-
[32mCJS[39m ⚡️ Build success in 133ms
|
|
15
|
-
[32mESM[39m [1mdist/index.mjs [22m[32m6.06 KB[39m
|
|
16
12
|
[32mESM[39m [1mdist/cli.mjs [22m[32m8.83 KB[39m
|
|
17
|
-
[32mESM[39m [1mdist/chunk-
|
|
18
|
-
[32mESM[39m
|
|
13
|
+
[32mESM[39m [1mdist/chunk-IXBC6GVT.mjs [22m[32m24.93 KB[39m
|
|
14
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m6.12 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 24ms
|
|
16
|
+
[32mCJS[39m [1mdist/index.js [22m[32m33.31 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/cli.js [22m[32m35.81 KB[39m
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 24ms
|
|
19
19
|
DTS Build start
|
|
20
|
-
DTS ⚡️ Build success in
|
|
20
|
+
DTS ⚡️ Build success in 1221ms
|
|
21
21
|
DTS dist/cli.d.ts 20.00 B
|
|
22
22
|
DTS dist/index.d.ts 3.34 KB
|
|
23
23
|
DTS dist/cli.d.mts 20.00 B
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @aiready/consistency@0.16.
|
|
3
|
+
> @aiready/consistency@0.16.4 test /Users/pengcao/projects/aiready/packages/consistency
|
|
4
4
|
> vitest run
|
|
5
5
|
|
|
6
6
|
[?25l
|
|
7
7
|
[1m[46m RUN [49m[22m [36mv4.0.18 [39m[90m/Users/pengcao/projects/aiready/packages/consistency[39m
|
|
8
8
|
|
|
9
9
|
[32m✓[39m dist/__tests__/scoring.test.js [2m([22m[2m8 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
10
|
-
[32m✓[39m src/__tests__/
|
|
11
|
-
[32m✓[39m src/__tests__/
|
|
12
|
-
[32m✓[39m
|
|
13
|
-
[32m✓[39m
|
|
14
|
-
[32m✓[39m src/__tests__/analyzer.test.ts [2m([22m[2m18 tests[22m[2m)[22m[
|
|
15
|
-
|
|
16
|
-
[32m✓[39m dist/__tests__/analyzer.test.js [2m([22m[2m18 tests[22m[2m)[22m[33m 620[2mms[22m[39m
|
|
17
|
-
[33m[2m✓[22m[39m should analyze naming issues [33m 376[2mms[22m[39m
|
|
10
|
+
[32m✓[39m src/__tests__/scoring.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
11
|
+
[32m✓[39m src/__tests__/language-filter.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
12
|
+
[32m✓[39m dist/__tests__/language-filter.test.js [2m([22m[2m3 tests[22m[2m)[22m[32m 7[2mms[22m[39m
|
|
13
|
+
[32m✓[39m src/__tests__/contract.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 13[2mms[22m[39m
|
|
14
|
+
[32m✓[39m src/__tests__/analyzer.test.ts [2m([22m[2m18 tests[22m[2m)[22m[32m 222[2mms[22m[39m
|
|
15
|
+
[32m✓[39m dist/__tests__/analyzer.test.js [2m([22m[2m18 tests[22m[2m)[22m[33m 686[2mms[22m[39m
|
|
18
16
|
|
|
19
17
|
[2m Test Files [22m [1m[32m7 passed[39m[22m[90m (7)[39m
|
|
20
18
|
[2m Tests [22m [1m[32m59 passed[39m[22m[90m (59)[39m
|
|
21
|
-
[2m Start at [22m
|
|
22
|
-
[2m Duration [22m
|
|
19
|
+
[2m Start at [22m 21:39:02
|
|
20
|
+
[2m Duration [22m 1.94s[2m (transform 1.08s, setup 0ms, import 5.25s, tests 939ms, environment 1ms)[22m
|
|
23
21
|
|
|
24
22
|
[?25h
|
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
// src/analyzers/naming-ast.ts
|
|
2
|
+
import { Severity as Severity2 } from "@aiready/core";
|
|
3
|
+
|
|
4
|
+
// src/utils/ast-parser.ts
|
|
5
|
+
import { parse } from "@typescript-eslint/typescript-estree";
|
|
6
|
+
import { readFileSync } from "fs";
|
|
7
|
+
function parseFile(filePath, content) {
|
|
8
|
+
try {
|
|
9
|
+
const code = content ?? readFileSync(filePath, "utf-8");
|
|
10
|
+
const isTypeScript = filePath.match(/\.tsx?$/);
|
|
11
|
+
return parse(code, {
|
|
12
|
+
jsx: filePath.match(/\.[jt]sx$/i) !== null,
|
|
13
|
+
loc: true,
|
|
14
|
+
range: true,
|
|
15
|
+
comment: false,
|
|
16
|
+
tokens: false,
|
|
17
|
+
// Relaxed parsing for JavaScript files
|
|
18
|
+
sourceType: "module",
|
|
19
|
+
ecmaVersion: "latest",
|
|
20
|
+
// Only use TypeScript parser features for .ts/.tsx files
|
|
21
|
+
filePath: isTypeScript ? filePath : void 0
|
|
22
|
+
});
|
|
23
|
+
} catch (error) {
|
|
24
|
+
void error;
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function traverseAST(node, visitor, parent = null) {
|
|
29
|
+
if (!node) return;
|
|
30
|
+
visitor.enter?.(node, parent);
|
|
31
|
+
for (const key of Object.keys(node)) {
|
|
32
|
+
const value = node[key];
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
for (const child of value) {
|
|
35
|
+
if (child && typeof child === "object" && "type" in child) {
|
|
36
|
+
traverseAST(child, visitor, node);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} else if (value && typeof value === "object" && "type" in value) {
|
|
40
|
+
traverseAST(value, visitor, node);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
visitor.leave?.(node, parent);
|
|
44
|
+
}
|
|
45
|
+
function isLoopStatement(node) {
|
|
46
|
+
return [
|
|
47
|
+
"ForStatement",
|
|
48
|
+
"ForInStatement",
|
|
49
|
+
"ForOfStatement",
|
|
50
|
+
"WhileStatement",
|
|
51
|
+
"DoWhileStatement"
|
|
52
|
+
].includes(node.type);
|
|
53
|
+
}
|
|
54
|
+
function getLineNumber(node) {
|
|
55
|
+
return node.loc?.start.line ?? 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/utils/context-detector.ts
|
|
59
|
+
import { Severity } from "@aiready/core";
|
|
60
|
+
function detectFileType(filePath, ast) {
|
|
61
|
+
void ast;
|
|
62
|
+
const path = filePath.toLowerCase();
|
|
63
|
+
if (path.match(/\.(test|spec)\.(ts|tsx|js|jsx)$/) || path.includes("__tests__")) {
|
|
64
|
+
return "test";
|
|
65
|
+
}
|
|
66
|
+
if (path.endsWith(".d.ts") || path.includes("types")) {
|
|
67
|
+
return "types";
|
|
68
|
+
}
|
|
69
|
+
if (path.match(/config|\.config\.|rc\.|setup/) || path.includes("configuration")) {
|
|
70
|
+
return "config";
|
|
71
|
+
}
|
|
72
|
+
return "production";
|
|
73
|
+
}
|
|
74
|
+
function detectCodeLayer(ast) {
|
|
75
|
+
let hasAPIIndicators = 0;
|
|
76
|
+
let hasBusinessIndicators = 0;
|
|
77
|
+
let hasDataIndicators = 0;
|
|
78
|
+
let hasUtilityIndicators = 0;
|
|
79
|
+
traverseAST(ast, {
|
|
80
|
+
enter: (node) => {
|
|
81
|
+
if (node.type === "ImportDeclaration") {
|
|
82
|
+
const source = node.source.value;
|
|
83
|
+
if (source.match(/express|fastify|koa|@nestjs|axios|fetch|http/i)) {
|
|
84
|
+
hasAPIIndicators++;
|
|
85
|
+
}
|
|
86
|
+
if (source.match(/database|prisma|typeorm|sequelize|mongoose|pg|mysql/i)) {
|
|
87
|
+
hasDataIndicators++;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (node.type === "FunctionDeclaration" && node.id) {
|
|
91
|
+
const name = node.id.name;
|
|
92
|
+
if (name.match(
|
|
93
|
+
/^(get|post|put|delete|patch|handle|api|route|controller)/i
|
|
94
|
+
)) {
|
|
95
|
+
hasAPIIndicators++;
|
|
96
|
+
}
|
|
97
|
+
if (name.match(/^(calculate|process|validate|transform|compute|analyze)/i)) {
|
|
98
|
+
hasBusinessIndicators++;
|
|
99
|
+
}
|
|
100
|
+
if (name.match(/^(find|create|update|delete|save|fetch|query|insert)/i)) {
|
|
101
|
+
hasDataIndicators++;
|
|
102
|
+
}
|
|
103
|
+
if (name.match(
|
|
104
|
+
/^(format|parse|convert|normalize|sanitize|encode|decode)/i
|
|
105
|
+
)) {
|
|
106
|
+
hasUtilityIndicators++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
|
|
110
|
+
if (node.type === "ExportNamedDeclaration" && node.declaration) {
|
|
111
|
+
if (node.declaration.type === "FunctionDeclaration" && node.declaration.id) {
|
|
112
|
+
const name = node.declaration.id.name;
|
|
113
|
+
if (name.match(/handler|route|api|controller/i)) {
|
|
114
|
+
hasAPIIndicators += 2;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
const scores = {
|
|
122
|
+
api: hasAPIIndicators,
|
|
123
|
+
business: hasBusinessIndicators,
|
|
124
|
+
data: hasDataIndicators,
|
|
125
|
+
utility: hasUtilityIndicators
|
|
126
|
+
};
|
|
127
|
+
const maxScore = Math.max(...Object.values(scores));
|
|
128
|
+
if (maxScore === 0) {
|
|
129
|
+
return "unknown";
|
|
130
|
+
}
|
|
131
|
+
if (scores.api === maxScore) return "api";
|
|
132
|
+
if (scores.data === maxScore) return "data";
|
|
133
|
+
if (scores.business === maxScore) return "business";
|
|
134
|
+
if (scores.utility === maxScore) return "utility";
|
|
135
|
+
return "unknown";
|
|
136
|
+
}
|
|
137
|
+
function calculateComplexity(node) {
|
|
138
|
+
let complexity = 1;
|
|
139
|
+
traverseAST(node, {
|
|
140
|
+
enter: (childNode) => {
|
|
141
|
+
switch (childNode.type) {
|
|
142
|
+
case "IfStatement":
|
|
143
|
+
case "ConditionalExpression":
|
|
144
|
+
// ternary
|
|
145
|
+
case "SwitchCase":
|
|
146
|
+
case "ForStatement":
|
|
147
|
+
case "ForInStatement":
|
|
148
|
+
case "ForOfStatement":
|
|
149
|
+
case "WhileStatement":
|
|
150
|
+
case "DoWhileStatement":
|
|
151
|
+
case "CatchClause":
|
|
152
|
+
complexity++;
|
|
153
|
+
break;
|
|
154
|
+
case "LogicalExpression":
|
|
155
|
+
if (childNode.operator === "&&" || childNode.operator === "||") {
|
|
156
|
+
complexity++;
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
return complexity;
|
|
163
|
+
}
|
|
164
|
+
function buildCodeContext(filePath, ast) {
|
|
165
|
+
const fileType = detectFileType(filePath, ast);
|
|
166
|
+
const codeLayer = detectCodeLayer(ast);
|
|
167
|
+
let totalComplexity = 0;
|
|
168
|
+
let functionCount = 0;
|
|
169
|
+
traverseAST(ast, {
|
|
170
|
+
enter: (node) => {
|
|
171
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
|
|
172
|
+
totalComplexity += calculateComplexity(node);
|
|
173
|
+
functionCount++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
const avgComplexity = functionCount > 0 ? totalComplexity / functionCount : 1;
|
|
178
|
+
return {
|
|
179
|
+
fileType,
|
|
180
|
+
codeLayer,
|
|
181
|
+
complexity: Math.round(avgComplexity),
|
|
182
|
+
isTestFile: fileType === "test",
|
|
183
|
+
isTypeDefinition: fileType === "types"
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function adjustSeverity(baseSeverity, context, issueType) {
|
|
187
|
+
const getEnum = (s) => {
|
|
188
|
+
if (s === Severity.Critical || s === "critical") return Severity.Critical;
|
|
189
|
+
if (s === Severity.Major || s === "major") return Severity.Major;
|
|
190
|
+
if (s === Severity.Minor || s === "minor") return Severity.Minor;
|
|
191
|
+
return Severity.Info;
|
|
192
|
+
};
|
|
193
|
+
let currentSev = getEnum(baseSeverity);
|
|
194
|
+
if (context.isTestFile) {
|
|
195
|
+
if (currentSev === Severity.Minor) currentSev = Severity.Info;
|
|
196
|
+
if (currentSev === Severity.Major) currentSev = Severity.Minor;
|
|
197
|
+
}
|
|
198
|
+
if (context.isTypeDefinition) {
|
|
199
|
+
if (currentSev === Severity.Minor) currentSev = Severity.Info;
|
|
200
|
+
}
|
|
201
|
+
if (context.codeLayer === "api") {
|
|
202
|
+
if (currentSev === Severity.Info && issueType === "unclear")
|
|
203
|
+
currentSev = Severity.Minor;
|
|
204
|
+
if (currentSev === Severity.Minor && issueType === "unclear")
|
|
205
|
+
currentSev = Severity.Major;
|
|
206
|
+
}
|
|
207
|
+
if (context.complexity > 10) {
|
|
208
|
+
if (currentSev === Severity.Info) currentSev = Severity.Minor;
|
|
209
|
+
}
|
|
210
|
+
if (context.codeLayer === "utility") {
|
|
211
|
+
if (currentSev === Severity.Minor && issueType === "abbreviation")
|
|
212
|
+
currentSev = Severity.Info;
|
|
213
|
+
}
|
|
214
|
+
return currentSev;
|
|
215
|
+
}
|
|
216
|
+
function isAcceptableInContext(name, context, options) {
|
|
217
|
+
if (options.isLoopVariable && ["i", "j", "k", "l", "n", "m"].includes(name)) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
if (context.isTestFile) {
|
|
221
|
+
if (["a", "b", "c", "x", "y", "z"].includes(name) && options.isParameter) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (context.codeLayer === "utility" && ["x", "y", "z"].includes(name)) {
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
if (options.isDestructured) {
|
|
229
|
+
if (["s", "b", "f", "l"].includes(name)) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (options.isParameter && (options.complexity ?? context.complexity) < 3) {
|
|
234
|
+
if (name.length >= 2) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/analyzers/naming-ast.ts
|
|
242
|
+
async function analyzeNamingAST(filePaths) {
|
|
243
|
+
const allIssues = [];
|
|
244
|
+
for (const filePath of filePaths) {
|
|
245
|
+
try {
|
|
246
|
+
const ast = parseFile(filePath);
|
|
247
|
+
if (!ast) continue;
|
|
248
|
+
const context = buildCodeContext(filePath, ast);
|
|
249
|
+
const issues = analyzeIdentifiers(ast, filePath, context);
|
|
250
|
+
allIssues.push(...issues);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
void err;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return allIssues;
|
|
256
|
+
}
|
|
257
|
+
function analyzeIdentifiers(ast, filePath, context) {
|
|
258
|
+
const issues = [];
|
|
259
|
+
const scopeTracker = new ScopeTracker();
|
|
260
|
+
traverseAST(ast, {
|
|
261
|
+
enter: (node) => {
|
|
262
|
+
if (node.type === "VariableDeclarator" && node.id.type === "Identifier") {
|
|
263
|
+
const isParameter = false;
|
|
264
|
+
const isLoopVariable = isLoopStatement(node.parent?.parent);
|
|
265
|
+
scopeTracker.declareVariable(
|
|
266
|
+
node.id.name,
|
|
267
|
+
node.id,
|
|
268
|
+
getLineNumber(node.id),
|
|
269
|
+
{ isParameter, isLoopVariable }
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
|
|
273
|
+
node.params.forEach((param) => {
|
|
274
|
+
if (param.type === "Identifier") {
|
|
275
|
+
scopeTracker.declareVariable(
|
|
276
|
+
param.name,
|
|
277
|
+
param,
|
|
278
|
+
getLineNumber(param),
|
|
279
|
+
{ isParameter: true }
|
|
280
|
+
);
|
|
281
|
+
} else if (param.type === "ObjectPattern") {
|
|
282
|
+
extractDestructuredIdentifiers(param, true, scopeTracker);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
if ((node.type === "ClassDeclaration" || node.type === "TSInterfaceDeclaration" || node.type === "TSTypeAliasDeclaration") && node.id) {
|
|
287
|
+
checkNamingConvention(
|
|
288
|
+
node.id.name,
|
|
289
|
+
"PascalCase",
|
|
290
|
+
node.id,
|
|
291
|
+
filePath,
|
|
292
|
+
issues,
|
|
293
|
+
context
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
for (const varInfo of scopeTracker.getVariables()) {
|
|
299
|
+
checkVariableNaming(varInfo, filePath, issues, context);
|
|
300
|
+
}
|
|
301
|
+
return issues;
|
|
302
|
+
}
|
|
303
|
+
function checkNamingConvention(name, convention, node, file, issues, context) {
|
|
304
|
+
let isValid = true;
|
|
305
|
+
if (convention === "PascalCase") {
|
|
306
|
+
isValid = /^[A-Z][a-zA-Z0-9]*$/.test(name);
|
|
307
|
+
} else if (convention === "camelCase") {
|
|
308
|
+
isValid = /^[a-z][a-zA-Z0-9]*$/.test(name);
|
|
309
|
+
} else if (convention === "UPPER_CASE") {
|
|
310
|
+
isValid = /^[A-Z][A-Z0-9_]*$/.test(name);
|
|
311
|
+
}
|
|
312
|
+
if (!isValid) {
|
|
313
|
+
const severity = adjustSeverity(Severity2.Info, context, "convention-mix");
|
|
314
|
+
issues.push({
|
|
315
|
+
file,
|
|
316
|
+
line: getLineNumber(node),
|
|
317
|
+
type: "convention-mix",
|
|
318
|
+
identifier: name,
|
|
319
|
+
severity,
|
|
320
|
+
suggestion: `Follow ${convention} for this identifier`
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function checkVariableNaming(varInfo, file, issues, context) {
|
|
325
|
+
const { name, node, line, options } = varInfo;
|
|
326
|
+
if (isAcceptableInContext(name, context, options)) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (name.length === 1 && !options.isLoopVariable) {
|
|
330
|
+
const severity = adjustSeverity(Severity2.Minor, context, "poor-naming");
|
|
331
|
+
issues.push({
|
|
332
|
+
file,
|
|
333
|
+
line,
|
|
334
|
+
type: "poor-naming",
|
|
335
|
+
identifier: name,
|
|
336
|
+
severity,
|
|
337
|
+
suggestion: "Use a more descriptive name than a single letter"
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
const vagueNames = [
|
|
341
|
+
"data",
|
|
342
|
+
"info",
|
|
343
|
+
"item",
|
|
344
|
+
"obj",
|
|
345
|
+
"val",
|
|
346
|
+
"tmp",
|
|
347
|
+
"temp",
|
|
348
|
+
"thing",
|
|
349
|
+
"stuff"
|
|
350
|
+
];
|
|
351
|
+
if (vagueNames.includes(name.toLowerCase())) {
|
|
352
|
+
const severity = adjustSeverity(Severity2.Minor, context, "poor-naming");
|
|
353
|
+
issues.push({
|
|
354
|
+
file,
|
|
355
|
+
line,
|
|
356
|
+
type: "poor-naming",
|
|
357
|
+
identifier: name,
|
|
358
|
+
severity,
|
|
359
|
+
suggestion: `Avoid vague names like '${name}'. What does this data represent?`
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
if (name.length > 1 && name.length <= 3 && !options.isLoopVariable && !isCommonAbbreviation(name)) {
|
|
363
|
+
const severity = adjustSeverity(Severity2.Info, context, "abbreviation");
|
|
364
|
+
issues.push({
|
|
365
|
+
file,
|
|
366
|
+
line,
|
|
367
|
+
type: "abbreviation",
|
|
368
|
+
identifier: name,
|
|
369
|
+
severity,
|
|
370
|
+
suggestion: "Avoid non-standard abbreviations"
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function isCommonAbbreviation(name) {
|
|
375
|
+
const common = [
|
|
376
|
+
"id",
|
|
377
|
+
"db",
|
|
378
|
+
"fs",
|
|
379
|
+
"os",
|
|
380
|
+
"ip",
|
|
381
|
+
"ui",
|
|
382
|
+
"ux",
|
|
383
|
+
"api",
|
|
384
|
+
"env",
|
|
385
|
+
"url"
|
|
386
|
+
];
|
|
387
|
+
return common.includes(name.toLowerCase());
|
|
388
|
+
}
|
|
389
|
+
var ScopeTracker = class {
|
|
390
|
+
constructor() {
|
|
391
|
+
this.variables = [];
|
|
392
|
+
}
|
|
393
|
+
declareVariable(name, node, line, options = {}) {
|
|
394
|
+
this.variables.push({ name, node, line, options });
|
|
395
|
+
}
|
|
396
|
+
getVariables() {
|
|
397
|
+
return this.variables;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
function extractDestructuredIdentifiers(node, isParameter, scopeTracker) {
|
|
401
|
+
if (node.type === "ObjectPattern") {
|
|
402
|
+
node.properties.forEach((prop) => {
|
|
403
|
+
if (prop.type === "Property" && prop.value.type === "Identifier") {
|
|
404
|
+
scopeTracker.declareVariable(
|
|
405
|
+
prop.value.name,
|
|
406
|
+
prop.value,
|
|
407
|
+
getLineNumber(prop.value),
|
|
408
|
+
{
|
|
409
|
+
isParameter,
|
|
410
|
+
isDestructured: true
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
} else if (node.type === "ArrayPattern") {
|
|
416
|
+
for (const element of node.elements) {
|
|
417
|
+
if (element?.type === "Identifier") {
|
|
418
|
+
scopeTracker.declareVariable(
|
|
419
|
+
element.name,
|
|
420
|
+
element,
|
|
421
|
+
getLineNumber(element),
|
|
422
|
+
{
|
|
423
|
+
isParameter,
|
|
424
|
+
isDestructured: true
|
|
425
|
+
}
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/analyzers/patterns.ts
|
|
433
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
434
|
+
import { Severity as Severity3 } from "@aiready/core";
|
|
435
|
+
async function analyzePatterns(filePaths) {
|
|
436
|
+
const issues = [];
|
|
437
|
+
const contents = /* @__PURE__ */ new Map();
|
|
438
|
+
const tryCatchPattern = /try\s*\{/g;
|
|
439
|
+
const catchPattern = /catch\s*\(\s*(\w+)\s*\)/g;
|
|
440
|
+
const styleStats = {
|
|
441
|
+
tryCatch: 0,
|
|
442
|
+
thenCatch: 0,
|
|
443
|
+
asyncAwait: 0,
|
|
444
|
+
commonJs: 0,
|
|
445
|
+
esm: 0
|
|
446
|
+
};
|
|
447
|
+
for (const filePath of filePaths) {
|
|
448
|
+
try {
|
|
449
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
450
|
+
contents.set(filePath, content);
|
|
451
|
+
if (content.match(tryCatchPattern)) styleStats.tryCatch++;
|
|
452
|
+
if (content.match(/\.catch\s*\(/)) styleStats.thenCatch++;
|
|
453
|
+
if (content.match(/\bawait\b/)) styleStats.asyncAwait++;
|
|
454
|
+
if (content.match(/\brequire\s*\(/)) styleStats.commonJs++;
|
|
455
|
+
if (content.match(/\bimport\b.*\bfrom\b/)) styleStats.esm++;
|
|
456
|
+
} catch (err) {
|
|
457
|
+
void err;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const totalFiles = filePaths.length;
|
|
461
|
+
if (styleStats.tryCatch > 0 && styleStats.thenCatch > 0) {
|
|
462
|
+
const dominant = styleStats.tryCatch >= styleStats.thenCatch ? "try-catch" : ".catch()";
|
|
463
|
+
const minority = dominant === "try-catch" ? ".catch()" : "try-catch";
|
|
464
|
+
issues.push({
|
|
465
|
+
files: filePaths.filter((f) => {
|
|
466
|
+
const c = contents.get(f) || "";
|
|
467
|
+
return minority === "try-catch" ? c.match(tryCatchPattern) : c.match(/\.catch\s*\(/);
|
|
468
|
+
}),
|
|
469
|
+
type: "error-handling",
|
|
470
|
+
description: `Mixed error handling styles: codebase primarily uses ${dominant}, but found ${minority} in some files.`,
|
|
471
|
+
examples: [dominant, minority],
|
|
472
|
+
severity: Severity3.Minor
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (styleStats.commonJs > 0 && styleStats.esm > 0) {
|
|
476
|
+
const minority = styleStats.esm >= styleStats.commonJs ? "CommonJS (require)" : "ESM (import)";
|
|
477
|
+
issues.push({
|
|
478
|
+
files: filePaths.filter((f) => {
|
|
479
|
+
const c = contents.get(f) || "";
|
|
480
|
+
return minority === "CommonJS (require)" ? c.match(/\brequire\s*\(/) : c.match(/\bimport\b/);
|
|
481
|
+
}),
|
|
482
|
+
type: "import-style",
|
|
483
|
+
description: `Mixed module systems: found both ESM and CommonJS.`,
|
|
484
|
+
examples: ['import X from "y"', 'const X = require("y")'],
|
|
485
|
+
severity: Severity3.Major
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
return issues;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/analyzer.ts
|
|
492
|
+
import { scanFiles, Severity as Severity4, IssueType } from "@aiready/core";
|
|
493
|
+
|
|
494
|
+
// src/analyzers/naming-python.ts
|
|
495
|
+
import { getParser } from "@aiready/core";
|
|
496
|
+
async function analyzePythonNaming(files) {
|
|
497
|
+
const issues = [];
|
|
498
|
+
const parser = getParser("dummy.py");
|
|
499
|
+
if (!parser) {
|
|
500
|
+
console.warn("Python parser not available");
|
|
501
|
+
return issues;
|
|
502
|
+
}
|
|
503
|
+
const pythonFiles = files.filter((f) => f.toLowerCase().endsWith(".py"));
|
|
504
|
+
for (const file of pythonFiles) {
|
|
505
|
+
try {
|
|
506
|
+
const fs = await import("fs");
|
|
507
|
+
const code = await fs.promises.readFile(file, "utf-8");
|
|
508
|
+
const result = parser.parse(code, file);
|
|
509
|
+
for (const exp of result.exports) {
|
|
510
|
+
const nameIssue = checkPythonNaming(
|
|
511
|
+
exp.name,
|
|
512
|
+
exp.type,
|
|
513
|
+
file,
|
|
514
|
+
exp.loc?.start.line || 0
|
|
515
|
+
);
|
|
516
|
+
if (nameIssue) {
|
|
517
|
+
issues.push(nameIssue);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
for (const imp of result.imports) {
|
|
521
|
+
for (const spec of imp.specifiers) {
|
|
522
|
+
if (spec !== "*" && spec !== "default") {
|
|
523
|
+
const nameIssue = checkPythonNaming(
|
|
524
|
+
spec,
|
|
525
|
+
"variable",
|
|
526
|
+
file,
|
|
527
|
+
imp.loc?.start.line || 0
|
|
528
|
+
);
|
|
529
|
+
if (nameIssue) {
|
|
530
|
+
issues.push(nameIssue);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
} catch (error) {
|
|
536
|
+
console.warn(`Skipping ${file} due to error:`, error);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return issues;
|
|
540
|
+
}
|
|
541
|
+
function checkPythonNaming(identifier, type, file, line) {
|
|
542
|
+
const parser = getParser("dummy.py");
|
|
543
|
+
const conventions = parser?.getNamingConventions();
|
|
544
|
+
if (!conventions) return null;
|
|
545
|
+
if (conventions.exceptions?.includes(identifier)) {
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
if (type === "class") {
|
|
549
|
+
if (!conventions.classPattern.test(identifier)) {
|
|
550
|
+
return {
|
|
551
|
+
type: "poor-naming",
|
|
552
|
+
identifier,
|
|
553
|
+
file,
|
|
554
|
+
line,
|
|
555
|
+
column: 0,
|
|
556
|
+
severity: "major",
|
|
557
|
+
category: "naming",
|
|
558
|
+
suggestion: `Class names should use PascalCase (e.g., ${toPascalCase(identifier)})`
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
} else if (type === "function") {
|
|
562
|
+
if (!conventions.functionPattern.test(identifier)) {
|
|
563
|
+
if (/^[a-z][a-zA-Z0-9]*$/.test(identifier) && /[A-Z]/.test(identifier)) {
|
|
564
|
+
return {
|
|
565
|
+
type: "convention-mix",
|
|
566
|
+
identifier,
|
|
567
|
+
file,
|
|
568
|
+
line,
|
|
569
|
+
column: 0,
|
|
570
|
+
severity: "major",
|
|
571
|
+
category: "naming",
|
|
572
|
+
suggestion: `Function names should use snake_case, not camelCase (e.g., ${toSnakeCase(identifier)})`
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
} else if (type === "const" || type === "variable") {
|
|
577
|
+
if (identifier === identifier.toUpperCase() && identifier.length > 1) {
|
|
578
|
+
if (!conventions.constantPattern.test(identifier)) {
|
|
579
|
+
return {
|
|
580
|
+
type: "poor-naming",
|
|
581
|
+
identifier,
|
|
582
|
+
file,
|
|
583
|
+
line,
|
|
584
|
+
column: 0,
|
|
585
|
+
severity: "minor",
|
|
586
|
+
category: "naming",
|
|
587
|
+
suggestion: "Constants should use UPPER_CASE_WITH_UNDERSCORES"
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
} else {
|
|
591
|
+
if (!conventions.variablePattern.test(identifier)) {
|
|
592
|
+
if (/^[a-z][a-zA-Z0-9]*$/.test(identifier) && /[A-Z]/.test(identifier)) {
|
|
593
|
+
return {
|
|
594
|
+
type: "convention-mix",
|
|
595
|
+
identifier,
|
|
596
|
+
file,
|
|
597
|
+
line,
|
|
598
|
+
column: 0,
|
|
599
|
+
severity: "major",
|
|
600
|
+
category: "naming",
|
|
601
|
+
suggestion: `Variable names should use snake_case, not camelCase (e.g., ${toSnakeCase(identifier)})`
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
function toSnakeCase(str) {
|
|
610
|
+
return str.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
611
|
+
}
|
|
612
|
+
function toPascalCase(str) {
|
|
613
|
+
return str.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/analyzer.ts
|
|
617
|
+
async function analyzeConsistency(options) {
|
|
618
|
+
const {
|
|
619
|
+
checkNaming = true,
|
|
620
|
+
checkPatterns = true,
|
|
621
|
+
checkArchitecture = false,
|
|
622
|
+
// Not implemented yet
|
|
623
|
+
minSeverity = Severity4.Info,
|
|
624
|
+
...scanOptions
|
|
625
|
+
} = options;
|
|
626
|
+
void checkArchitecture;
|
|
627
|
+
const filePaths = await scanFiles(scanOptions);
|
|
628
|
+
const tsJsFiles = filePaths.filter((f) => /\.(ts|tsx|js|jsx)$/i.test(f));
|
|
629
|
+
const pythonFiles = filePaths.filter((f) => /\.py$/i.test(f));
|
|
630
|
+
let namingIssues = [];
|
|
631
|
+
if (checkNaming) {
|
|
632
|
+
const tsJsNamingIssues = tsJsFiles.length > 0 ? await analyzeNamingAST(tsJsFiles) : [];
|
|
633
|
+
const pythonNamingIssues = pythonFiles.length > 0 ? await analyzePythonNaming(pythonFiles) : [];
|
|
634
|
+
namingIssues = [...tsJsNamingIssues, ...pythonNamingIssues];
|
|
635
|
+
}
|
|
636
|
+
const patternIssues = checkPatterns ? await analyzePatterns(filePaths) : [];
|
|
637
|
+
const results = [];
|
|
638
|
+
const fileIssuesMap = /* @__PURE__ */ new Map();
|
|
639
|
+
for (const issue of namingIssues) {
|
|
640
|
+
if (!shouldIncludeSeverity(issue.severity, minSeverity)) {
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
const consistencyIssue = {
|
|
644
|
+
type: issue.type === "convention-mix" ? IssueType.NamingInconsistency : IssueType.NamingQuality,
|
|
645
|
+
category: "naming",
|
|
646
|
+
severity: getSeverityEnum(issue.severity),
|
|
647
|
+
message: `${issue.type}: ${issue.identifier}`,
|
|
648
|
+
location: {
|
|
649
|
+
file: issue.file,
|
|
650
|
+
line: issue.line,
|
|
651
|
+
column: issue.column
|
|
652
|
+
},
|
|
653
|
+
suggestion: issue.suggestion
|
|
654
|
+
};
|
|
655
|
+
if (!fileIssuesMap.has(issue.file)) {
|
|
656
|
+
fileIssuesMap.set(issue.file, []);
|
|
657
|
+
}
|
|
658
|
+
fileIssuesMap.get(issue.file).push(consistencyIssue);
|
|
659
|
+
}
|
|
660
|
+
for (const issue of patternIssues) {
|
|
661
|
+
if (!shouldIncludeSeverity(issue.severity, minSeverity)) {
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
const consistencyIssue = {
|
|
665
|
+
type: IssueType.PatternInconsistency,
|
|
666
|
+
category: "patterns",
|
|
667
|
+
severity: getSeverityEnum(issue.severity),
|
|
668
|
+
message: issue.description,
|
|
669
|
+
location: {
|
|
670
|
+
file: issue.files[0] || "multiple files",
|
|
671
|
+
line: 1
|
|
672
|
+
},
|
|
673
|
+
examples: issue.examples,
|
|
674
|
+
suggestion: `Standardize ${issue.type} patterns across ${issue.files.length} files`
|
|
675
|
+
};
|
|
676
|
+
const firstFile = issue.files[0];
|
|
677
|
+
if (firstFile && !fileIssuesMap.has(firstFile)) {
|
|
678
|
+
fileIssuesMap.set(firstFile, []);
|
|
679
|
+
}
|
|
680
|
+
if (firstFile) {
|
|
681
|
+
fileIssuesMap.get(firstFile).push(consistencyIssue);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
for (const [fileName, issues] of fileIssuesMap) {
|
|
685
|
+
results.push({
|
|
686
|
+
fileName,
|
|
687
|
+
issues,
|
|
688
|
+
metrics: {
|
|
689
|
+
consistencyScore: calculateConsistencyScore(issues)
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
results.sort((fileResultA, fileResultB) => {
|
|
694
|
+
const maxSeverityA = Math.min(
|
|
695
|
+
...fileResultA.issues.map((i) => {
|
|
696
|
+
const val = getSeverityLevel(i.severity);
|
|
697
|
+
return val === 4 ? 0 : val === 3 ? 1 : val === 2 ? 2 : 3;
|
|
698
|
+
})
|
|
699
|
+
);
|
|
700
|
+
const maxSeverityB = Math.min(
|
|
701
|
+
...fileResultB.issues.map((i) => {
|
|
702
|
+
const val = getSeverityLevel(i.severity);
|
|
703
|
+
return val === 4 ? 0 : val === 3 ? 1 : val === 2 ? 2 : 3;
|
|
704
|
+
})
|
|
705
|
+
);
|
|
706
|
+
if (maxSeverityA !== maxSeverityB) {
|
|
707
|
+
return maxSeverityA - maxSeverityB;
|
|
708
|
+
}
|
|
709
|
+
return fileResultB.issues.length - fileResultA.issues.length;
|
|
710
|
+
});
|
|
711
|
+
const recommendations = generateRecommendations(namingIssues, patternIssues);
|
|
712
|
+
const namingCountFiltered = namingIssues.filter(
|
|
713
|
+
(i) => shouldIncludeSeverity(i.severity, minSeverity)
|
|
714
|
+
).length;
|
|
715
|
+
const patternCountFiltered = patternIssues.filter(
|
|
716
|
+
(i) => shouldIncludeSeverity(i.severity, minSeverity)
|
|
717
|
+
).length;
|
|
718
|
+
return {
|
|
719
|
+
summary: {
|
|
720
|
+
totalIssues: namingCountFiltered + patternCountFiltered,
|
|
721
|
+
namingIssues: namingCountFiltered,
|
|
722
|
+
patternIssues: patternCountFiltered,
|
|
723
|
+
architectureIssues: 0,
|
|
724
|
+
filesAnalyzed: filePaths.length
|
|
725
|
+
},
|
|
726
|
+
results,
|
|
727
|
+
recommendations
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
function getSeverityLevel(s) {
|
|
731
|
+
if (s === Severity4.Critical || s === "critical") return 4;
|
|
732
|
+
if (s === Severity4.Major || s === "major") return 3;
|
|
733
|
+
if (s === Severity4.Minor || s === "minor") return 2;
|
|
734
|
+
if (s === Severity4.Info || s === "info") return 1;
|
|
735
|
+
return 0;
|
|
736
|
+
}
|
|
737
|
+
function getSeverityEnum(s) {
|
|
738
|
+
const val = getSeverityLevel(s);
|
|
739
|
+
switch (val) {
|
|
740
|
+
case 4:
|
|
741
|
+
return Severity4.Critical;
|
|
742
|
+
case 3:
|
|
743
|
+
return Severity4.Major;
|
|
744
|
+
case 2:
|
|
745
|
+
return Severity4.Minor;
|
|
746
|
+
case 1:
|
|
747
|
+
return Severity4.Info;
|
|
748
|
+
default:
|
|
749
|
+
return Severity4.Info;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
function shouldIncludeSeverity(severity, minSeverity) {
|
|
753
|
+
return getSeverityLevel(severity) >= getSeverityLevel(minSeverity);
|
|
754
|
+
}
|
|
755
|
+
function calculateConsistencyScore(issues) {
|
|
756
|
+
let totalWeight = 0;
|
|
757
|
+
for (const issue of issues) {
|
|
758
|
+
const val = getSeverityLevel(issue.severity);
|
|
759
|
+
switch (val) {
|
|
760
|
+
case 4:
|
|
761
|
+
totalWeight += 10;
|
|
762
|
+
break;
|
|
763
|
+
case 3:
|
|
764
|
+
totalWeight += 5;
|
|
765
|
+
break;
|
|
766
|
+
case 2:
|
|
767
|
+
totalWeight += 2;
|
|
768
|
+
break;
|
|
769
|
+
case 1:
|
|
770
|
+
totalWeight += 1;
|
|
771
|
+
break;
|
|
772
|
+
default:
|
|
773
|
+
totalWeight += 1;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return Math.max(0, 1 - totalWeight / 100);
|
|
777
|
+
}
|
|
778
|
+
function generateRecommendations(namingIssues, patternIssues) {
|
|
779
|
+
const recommendations = [];
|
|
780
|
+
if (namingIssues.length > 0) {
|
|
781
|
+
const conventionMixCount = namingIssues.filter(
|
|
782
|
+
(i) => i.type === "convention-mix"
|
|
783
|
+
).length;
|
|
784
|
+
if (conventionMixCount > 0) {
|
|
785
|
+
recommendations.push(
|
|
786
|
+
`Standardize naming conventions: Found ${conventionMixCount} snake_case variables in TypeScript/JavaScript (use camelCase)`
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
const poorNamingCount = namingIssues.filter(
|
|
790
|
+
(i) => i.type === "poor-naming"
|
|
791
|
+
).length;
|
|
792
|
+
if (poorNamingCount > 0) {
|
|
793
|
+
recommendations.push(
|
|
794
|
+
`Improve variable naming: Found ${poorNamingCount} single-letter or unclear variable names`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (patternIssues.length > 0) {
|
|
799
|
+
const errorHandlingIssues = patternIssues.filter(
|
|
800
|
+
(i) => i.type === "error-handling"
|
|
801
|
+
);
|
|
802
|
+
if (errorHandlingIssues.length > 0) {
|
|
803
|
+
recommendations.push(
|
|
804
|
+
"Standardize error handling strategy across the codebase (prefer try-catch with typed errors)"
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
const asyncIssues = patternIssues.filter((i) => i.type === "async-style");
|
|
808
|
+
if (asyncIssues.length > 0) {
|
|
809
|
+
recommendations.push(
|
|
810
|
+
"Use async/await consistently instead of mixing with promise chains or callbacks"
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
const importIssues = patternIssues.filter((i) => i.type === "import-style");
|
|
814
|
+
if (importIssues.length > 0) {
|
|
815
|
+
recommendations.push(
|
|
816
|
+
"Use ES modules consistently across the project (avoid mixing with CommonJS)"
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
if (recommendations.length === 0) {
|
|
821
|
+
recommendations.push(
|
|
822
|
+
"No major consistency issues found! Your codebase follows good practices."
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
return recommendations;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
export {
|
|
829
|
+
analyzeNamingAST,
|
|
830
|
+
analyzePatterns,
|
|
831
|
+
analyzeConsistency
|
|
832
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -403,7 +403,18 @@ function checkVariableNaming(varInfo, file, issues, context) {
|
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
405
|
function isCommonAbbreviation(name) {
|
|
406
|
-
const common = [
|
|
406
|
+
const common = [
|
|
407
|
+
"id",
|
|
408
|
+
"db",
|
|
409
|
+
"fs",
|
|
410
|
+
"os",
|
|
411
|
+
"ip",
|
|
412
|
+
"ui",
|
|
413
|
+
"ux",
|
|
414
|
+
"api",
|
|
415
|
+
"env",
|
|
416
|
+
"url"
|
|
417
|
+
];
|
|
407
418
|
return common.includes(name.toLowerCase());
|
|
408
419
|
}
|
|
409
420
|
var ScopeTracker = class {
|
package/dist/cli.mjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -416,7 +416,18 @@ function checkVariableNaming(varInfo, file, issues, context) {
|
|
|
416
416
|
}
|
|
417
417
|
}
|
|
418
418
|
function isCommonAbbreviation(name) {
|
|
419
|
-
const common = [
|
|
419
|
+
const common = [
|
|
420
|
+
"id",
|
|
421
|
+
"db",
|
|
422
|
+
"fs",
|
|
423
|
+
"os",
|
|
424
|
+
"ip",
|
|
425
|
+
"ui",
|
|
426
|
+
"ux",
|
|
427
|
+
"api",
|
|
428
|
+
"env",
|
|
429
|
+
"url"
|
|
430
|
+
];
|
|
420
431
|
return common.includes(name.toLowerCase());
|
|
421
432
|
}
|
|
422
433
|
var ScopeTracker = class {
|
|
@@ -865,7 +876,9 @@ async function analyzeNaming(filePaths) {
|
|
|
865
876
|
const content = (0, import_fs3.readFileSync)(filePath, "utf-8");
|
|
866
877
|
const lines = content.split("\n");
|
|
867
878
|
lines.forEach((line, index) => {
|
|
868
|
-
const singleLetterMatch = line.match(
|
|
879
|
+
const singleLetterMatch = line.match(
|
|
880
|
+
/\b(const|let|var)\s+([a-hj-km-np-zA-Z])\s*=/
|
|
881
|
+
);
|
|
869
882
|
if (singleLetterMatch) {
|
|
870
883
|
issues.push({
|
|
871
884
|
file: filePath,
|
|
@@ -877,7 +890,9 @@ async function analyzeNaming(filePaths) {
|
|
|
877
890
|
});
|
|
878
891
|
}
|
|
879
892
|
if (filePath.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
880
|
-
const snakeCaseMatch = line.match(
|
|
893
|
+
const snakeCaseMatch = line.match(
|
|
894
|
+
/\b(const|let|var|function)\s+([a-z]+_[a-z0-9_]+)\b/
|
|
895
|
+
);
|
|
881
896
|
if (snakeCaseMatch) {
|
|
882
897
|
issues.push({
|
|
883
898
|
file: filePath,
|
|
@@ -889,7 +904,9 @@ async function analyzeNaming(filePaths) {
|
|
|
889
904
|
});
|
|
890
905
|
}
|
|
891
906
|
}
|
|
892
|
-
const shortNameMatch = line.match(
|
|
907
|
+
const shortNameMatch = line.match(
|
|
908
|
+
/\b(const|let|var)\s+([a-zA-Z0-9]{2,3})\s*=/
|
|
909
|
+
);
|
|
893
910
|
if (shortNameMatch) {
|
|
894
911
|
const name = shortNameMatch[2].toLowerCase();
|
|
895
912
|
const vagueNames = ["obj", "val", "tmp", "res", "ret", "data"];
|
package/dist/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
analyzeConsistency,
|
|
3
3
|
analyzeNamingAST,
|
|
4
4
|
analyzePatterns
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-IXBC6GVT.mjs";
|
|
6
6
|
|
|
7
7
|
// src/analyzers/naming.ts
|
|
8
8
|
import { readFileSync } from "fs";
|
|
@@ -14,7 +14,9 @@ async function analyzeNaming(filePaths) {
|
|
|
14
14
|
const content = readFileSync(filePath, "utf-8");
|
|
15
15
|
const lines = content.split("\n");
|
|
16
16
|
lines.forEach((line, index) => {
|
|
17
|
-
const singleLetterMatch = line.match(
|
|
17
|
+
const singleLetterMatch = line.match(
|
|
18
|
+
/\b(const|let|var)\s+([a-hj-km-np-zA-Z])\s*=/
|
|
19
|
+
);
|
|
18
20
|
if (singleLetterMatch) {
|
|
19
21
|
issues.push({
|
|
20
22
|
file: filePath,
|
|
@@ -26,7 +28,9 @@ async function analyzeNaming(filePaths) {
|
|
|
26
28
|
});
|
|
27
29
|
}
|
|
28
30
|
if (filePath.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
29
|
-
const snakeCaseMatch = line.match(
|
|
31
|
+
const snakeCaseMatch = line.match(
|
|
32
|
+
/\b(const|let|var|function)\s+([a-z]+_[a-z0-9_]+)\b/
|
|
33
|
+
);
|
|
30
34
|
if (snakeCaseMatch) {
|
|
31
35
|
issues.push({
|
|
32
36
|
file: filePath,
|
|
@@ -38,7 +42,9 @@ async function analyzeNaming(filePaths) {
|
|
|
38
42
|
});
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
|
-
const shortNameMatch = line.match(
|
|
45
|
+
const shortNameMatch = line.match(
|
|
46
|
+
/\b(const|let|var)\s+([a-zA-Z0-9]{2,3})\s*=/
|
|
47
|
+
);
|
|
42
48
|
if (shortNameMatch) {
|
|
43
49
|
const name = shortNameMatch[2].toLowerCase();
|
|
44
50
|
const vagueNames = ["obj", "val", "tmp", "res", "ret", "data"];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/consistency",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.5",
|
|
4
4
|
"description": "Detects consistency issues in naming, patterns, and architecture that confuse AI models",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@typescript-eslint/typescript-estree": "^8.53.0",
|
|
44
44
|
"chalk": "^5.3.0",
|
|
45
45
|
"commander": "^14.0.0",
|
|
46
|
-
"@aiready/core": "0.19.
|
|
46
|
+
"@aiready/core": "0.19.5"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/node": "^24.0.0",
|
package/src/analyzer.ts
CHANGED
|
@@ -123,15 +123,15 @@ export async function analyzeConsistency(
|
|
|
123
123
|
// Get highest severity in each file
|
|
124
124
|
const maxSeverityA = Math.min(
|
|
125
125
|
...fileResultA.issues.map((i) => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
const val = getSeverityLevel((i as ConsistencyIssue).severity);
|
|
127
|
+
// Map 4->0, 3->1, 2->2, 1->3
|
|
128
|
+
return val === 4 ? 0 : val === 3 ? 1 : val === 2 ? 2 : 3;
|
|
129
129
|
})
|
|
130
130
|
);
|
|
131
131
|
const maxSeverityB = Math.min(
|
|
132
132
|
...fileResultB.issues.map((i) => {
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
const val = getSeverityLevel((i as ConsistencyIssue).severity);
|
|
134
|
+
return val === 4 ? 0 : val === 3 ? 1 : val === 2 ? 2 : 3;
|
|
135
135
|
})
|
|
136
136
|
);
|
|
137
137
|
|
|
@@ -179,11 +179,16 @@ function getSeverityLevel(s: any): number {
|
|
|
179
179
|
function getSeverityEnum(s: any): Severity {
|
|
180
180
|
const val = getSeverityLevel(s);
|
|
181
181
|
switch (val) {
|
|
182
|
-
case 4:
|
|
183
|
-
|
|
184
|
-
case
|
|
185
|
-
|
|
186
|
-
|
|
182
|
+
case 4:
|
|
183
|
+
return Severity.Critical;
|
|
184
|
+
case 3:
|
|
185
|
+
return Severity.Major;
|
|
186
|
+
case 2:
|
|
187
|
+
return Severity.Minor;
|
|
188
|
+
case 1:
|
|
189
|
+
return Severity.Info;
|
|
190
|
+
default:
|
|
191
|
+
return Severity.Info;
|
|
187
192
|
}
|
|
188
193
|
}
|
|
189
194
|
|
|
@@ -199,11 +204,20 @@ function calculateConsistencyScore(issues: ConsistencyIssue[]): number {
|
|
|
199
204
|
for (const issue of issues) {
|
|
200
205
|
const val = getSeverityLevel(issue.severity);
|
|
201
206
|
switch (val) {
|
|
202
|
-
case 4:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
case
|
|
206
|
-
|
|
207
|
+
case 4:
|
|
208
|
+
totalWeight += 10;
|
|
209
|
+
break;
|
|
210
|
+
case 3:
|
|
211
|
+
totalWeight += 5;
|
|
212
|
+
break;
|
|
213
|
+
case 2:
|
|
214
|
+
totalWeight += 2;
|
|
215
|
+
break;
|
|
216
|
+
case 1:
|
|
217
|
+
totalWeight += 1;
|
|
218
|
+
break;
|
|
219
|
+
default:
|
|
220
|
+
totalWeight += 1;
|
|
207
221
|
}
|
|
208
222
|
}
|
|
209
223
|
// Score from 0-1, where 1 is perfect
|
|
@@ -218,7 +218,18 @@ function checkVariableNaming(
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
function isCommonAbbreviation(name: string): boolean {
|
|
221
|
-
const common = [
|
|
221
|
+
const common = [
|
|
222
|
+
'id',
|
|
223
|
+
'db',
|
|
224
|
+
'fs',
|
|
225
|
+
'os',
|
|
226
|
+
'ip',
|
|
227
|
+
'ui',
|
|
228
|
+
'ux',
|
|
229
|
+
'api',
|
|
230
|
+
'env',
|
|
231
|
+
'url',
|
|
232
|
+
];
|
|
222
233
|
return common.includes(name.toLowerCase());
|
|
223
234
|
}
|
|
224
235
|
|
|
@@ -228,7 +239,12 @@ function isCommonAbbreviation(name: string): boolean {
|
|
|
228
239
|
class ScopeTracker {
|
|
229
240
|
private variables: any[] = [];
|
|
230
241
|
|
|
231
|
-
declareVariable(
|
|
242
|
+
declareVariable(
|
|
243
|
+
name: string,
|
|
244
|
+
node: TSESTree.Node,
|
|
245
|
+
line: number,
|
|
246
|
+
options = {}
|
|
247
|
+
) {
|
|
232
248
|
this.variables.push({ name, node, line, options });
|
|
233
249
|
}
|
|
234
250
|
|
package/src/analyzers/naming.ts
CHANGED
|
@@ -6,7 +6,9 @@ import type { NamingIssue } from '../types';
|
|
|
6
6
|
* Legacy regex-based naming analyzer
|
|
7
7
|
* (Used as fallback or for languages without AST support)
|
|
8
8
|
*/
|
|
9
|
-
export async function analyzeNaming(
|
|
9
|
+
export async function analyzeNaming(
|
|
10
|
+
filePaths: string[]
|
|
11
|
+
): Promise<NamingIssue[]> {
|
|
10
12
|
const issues: NamingIssue[] = [];
|
|
11
13
|
|
|
12
14
|
for (const filePath of filePaths) {
|
|
@@ -16,9 +18,11 @@ export async function analyzeNaming(filePaths: string[]): Promise<NamingIssue[]>
|
|
|
16
18
|
|
|
17
19
|
lines.forEach((line, index) => {
|
|
18
20
|
// Simple regex patterns for naming issues
|
|
19
|
-
|
|
21
|
+
|
|
20
22
|
// 1. Single letter variables (except common ones)
|
|
21
|
-
const singleLetterMatch = line.match(
|
|
23
|
+
const singleLetterMatch = line.match(
|
|
24
|
+
/\b(const|let|var)\s+([a-hj-km-np-zA-Z])\s*=/
|
|
25
|
+
);
|
|
22
26
|
if (singleLetterMatch) {
|
|
23
27
|
issues.push({
|
|
24
28
|
file: filePath,
|
|
@@ -32,7 +36,9 @@ export async function analyzeNaming(filePaths: string[]): Promise<NamingIssue[]>
|
|
|
32
36
|
|
|
33
37
|
// 2. Snake case in TS/JS files
|
|
34
38
|
if (filePath.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
35
|
-
const snakeCaseMatch = line.match(
|
|
39
|
+
const snakeCaseMatch = line.match(
|
|
40
|
+
/\b(const|let|var|function)\s+([a-z]+_[a-z0-9_]+)\b/
|
|
41
|
+
);
|
|
36
42
|
if (snakeCaseMatch) {
|
|
37
43
|
issues.push({
|
|
38
44
|
file: filePath,
|
|
@@ -40,13 +46,16 @@ export async function analyzeNaming(filePaths: string[]): Promise<NamingIssue[]>
|
|
|
40
46
|
type: 'convention-mix',
|
|
41
47
|
identifier: snakeCaseMatch[2],
|
|
42
48
|
severity: Severity.Info,
|
|
43
|
-
suggestion:
|
|
49
|
+
suggestion:
|
|
50
|
+
'Use camelCase instead of snake_case in TypeScript/JavaScript',
|
|
44
51
|
});
|
|
45
52
|
}
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
// 3. Very short names
|
|
49
|
-
const shortNameMatch = line.match(
|
|
56
|
+
const shortNameMatch = line.match(
|
|
57
|
+
/\b(const|let|var)\s+([a-zA-Z0-9]{2,3})\s*=/
|
|
58
|
+
);
|
|
50
59
|
if (shortNameMatch) {
|
|
51
60
|
const name = shortNameMatch[2].toLowerCase();
|
|
52
61
|
const vagueNames = ['obj', 'val', 'tmp', 'res', 'ret', 'data'];
|
|
@@ -5,14 +5,16 @@ import type { PatternIssue } from '../types';
|
|
|
5
5
|
/**
|
|
6
6
|
* Detect inconsistent code patterns across files
|
|
7
7
|
*/
|
|
8
|
-
export async function analyzePatterns(
|
|
8
|
+
export async function analyzePatterns(
|
|
9
|
+
filePaths: string[]
|
|
10
|
+
): Promise<PatternIssue[]> {
|
|
9
11
|
const issues: PatternIssue[] = [];
|
|
10
12
|
const contents = new Map<string, string>();
|
|
11
13
|
|
|
12
14
|
// 1. Error handling style
|
|
13
15
|
const tryCatchPattern = /try\s*\{/g;
|
|
14
16
|
const catchPattern = /catch\s*\(\s*(\w+)\s*\)/g;
|
|
15
|
-
|
|
17
|
+
|
|
16
18
|
const styleStats = {
|
|
17
19
|
tryCatch: 0,
|
|
18
20
|
thenCatch: 0,
|
|
@@ -29,7 +31,7 @@ export async function analyzePatterns(filePaths: string[]): Promise<PatternIssue
|
|
|
29
31
|
if (content.match(tryCatchPattern)) styleStats.tryCatch++;
|
|
30
32
|
if (content.match(/\.catch\s*\(/)) styleStats.thenCatch++;
|
|
31
33
|
if (content.match(/\bawait\b/)) styleStats.asyncAwait++;
|
|
32
|
-
|
|
34
|
+
|
|
33
35
|
if (content.match(/\brequire\s*\(/)) styleStats.commonJs++;
|
|
34
36
|
if (content.match(/\bimport\b.*\bfrom\b/)) styleStats.esm++;
|
|
35
37
|
} catch (err) {
|
|
@@ -41,13 +43,16 @@ export async function analyzePatterns(filePaths: string[]): Promise<PatternIssue
|
|
|
41
43
|
|
|
42
44
|
// Report inconsistencies if there's a significant mix
|
|
43
45
|
if (styleStats.tryCatch > 0 && styleStats.thenCatch > 0) {
|
|
44
|
-
const dominant =
|
|
46
|
+
const dominant =
|
|
47
|
+
styleStats.tryCatch >= styleStats.thenCatch ? 'try-catch' : '.catch()';
|
|
45
48
|
const minority = dominant === 'try-catch' ? '.catch()' : 'try-catch';
|
|
46
|
-
|
|
49
|
+
|
|
47
50
|
issues.push({
|
|
48
|
-
files: filePaths.filter(f => {
|
|
51
|
+
files: filePaths.filter((f) => {
|
|
49
52
|
const c = contents.get(f) || '';
|
|
50
|
-
return minority === 'try-catch'
|
|
53
|
+
return minority === 'try-catch'
|
|
54
|
+
? c.match(tryCatchPattern)
|
|
55
|
+
: c.match(/\.catch\s*\(/);
|
|
51
56
|
}),
|
|
52
57
|
type: 'error-handling',
|
|
53
58
|
description: `Mixed error handling styles: codebase primarily uses ${dominant}, but found ${minority} in some files.`,
|
|
@@ -57,11 +62,16 @@ export async function analyzePatterns(filePaths: string[]): Promise<PatternIssue
|
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
if (styleStats.commonJs > 0 && styleStats.esm > 0) {
|
|
60
|
-
const minority =
|
|
65
|
+
const minority =
|
|
66
|
+
styleStats.esm >= styleStats.commonJs
|
|
67
|
+
? 'CommonJS (require)'
|
|
68
|
+
: 'ESM (import)';
|
|
61
69
|
issues.push({
|
|
62
|
-
files: filePaths.filter(f => {
|
|
70
|
+
files: filePaths.filter((f) => {
|
|
63
71
|
const c = contents.get(f) || '';
|
|
64
|
-
return minority === 'CommonJS (require)'
|
|
72
|
+
return minority === 'CommonJS (require)'
|
|
73
|
+
? c.match(/\brequire\s*\(/)
|
|
74
|
+
: c.match(/\bimport\b/);
|
|
65
75
|
}),
|
|
66
76
|
type: 'import-style',
|
|
67
77
|
description: `Mixed module systems: found both ESM and CommonJS.`,
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ScanOptions,
|
|
3
|
+
AnalysisResult,
|
|
4
|
+
Issue,
|
|
5
|
+
Severity,
|
|
6
|
+
IssueType,
|
|
7
|
+
} from '@aiready/core';
|
|
2
8
|
|
|
3
9
|
export interface ConsistencyOptions extends ScanOptions {
|
|
4
10
|
/** Check naming conventions and quality */
|