@aiready/consistency 0.20.3 → 0.20.4
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 +7 -7
- package/.turbo/turbo-test.log +12 -14
- package/dist/chunk-S6BZVTWN.mjs +731 -0
- package/dist/cli.js +3 -1
- package/dist/cli.mjs +1 -1
- package/dist/index.js +3 -1
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/analyzer.ts +3 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @aiready/consistency@0.20.
|
|
3
|
+
> @aiready/consistency@0.20.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
|
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
[34mCJS[39m Build start
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
[32mESM[39m [1mdist/index.mjs [22m[32m7.04 KB[39m
|
|
13
|
-
[32mESM[39m [1mdist/chunk-
|
|
13
|
+
[32mESM[39m [1mdist/chunk-S6BZVTWN.mjs [22m[32m22.19 KB[39m
|
|
14
14
|
[32mESM[39m [1mdist/cli.mjs [22m[32m8.83 KB[39m
|
|
15
|
-
[32mESM[39m ⚡️ Build success in
|
|
16
|
-
[32mCJS[39m [1mdist/
|
|
17
|
-
[32mCJS[39m [1mdist/
|
|
18
|
-
[32mCJS[39m ⚡️ Build success in
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 62ms
|
|
16
|
+
[32mCJS[39m [1mdist/index.js [22m[32m30.95 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/cli.js [22m[32m33.06 KB[39m
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 62ms
|
|
19
19
|
DTS Build start
|
|
20
|
-
DTS ⚡️ Build success in
|
|
20
|
+
DTS ⚡️ Build success in 3717ms
|
|
21
21
|
DTS dist/cli.d.ts 20.00 B
|
|
22
22
|
DTS dist/index.d.ts 3.65 KB
|
|
23
23
|
DTS dist/cli.d.mts 20.00 B
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,29 +1,27 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @aiready/consistency@0.20.
|
|
3
|
+
> @aiready/consistency@0.20.3 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
|
-
[32m✓[39m dist/__tests__/scoring.test.js [2m([22m[2m8 tests[22m[2m)[22m[32m
|
|
10
|
-
[32m✓[39m src/__tests__/provider.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 3[2mms[22m[39m
|
|
11
|
-
[32m✓[39m src/__tests__/language-filter.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 14[2mms[22m[39m
|
|
12
|
-
[32m✓[39m src/__tests__/scoring.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
13
|
-
[32m✓[39m dist/__tests__/language-filter.test.js [2m([22m[2m3 tests[22m[2m)[22m[32m 15[2mms[22m[39m
|
|
14
|
-
Sourcemap for "/Users/pengcao/projects/aiready/packages/consistency/dist/analyzers/naming-python.js" points to missing source files
|
|
9
|
+
[32m✓[39m dist/__tests__/scoring.test.js [2m([22m[2m8 tests[22m[2m)[22m[32m 3[2mms[22m[39m
|
|
15
10
|
[90mstderr[2m | src/__tests__/contract.test.ts[2m > [22m[2mConsistency Spoke Contract Validation[2m > [22m[2mshould produce output matching the SpokeOutput contract
|
|
16
11
|
[22m[39mConsistency: Failed to analyze file1.ts: Error: ENOENT: no such file or directory, open 'file1.ts'
|
|
17
12
|
|
|
18
|
-
[32m✓[39m src/__tests__/contract.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m
|
|
19
|
-
[32m✓[39m src/__tests__/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
[32m✓[39m src/__tests__/contract.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 6[2mms[22m[39m
|
|
14
|
+
[32m✓[39m src/__tests__/language-filter.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 7[2mms[22m[39m
|
|
15
|
+
[32m✓[39m src/__tests__/provider.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 7[2mms[22m[39m
|
|
16
|
+
Sourcemap for "/Users/pengcao/projects/aiready/packages/consistency/dist/analyzers/naming-python.js" points to missing source files
|
|
17
|
+
[32m✓[39m src/__tests__/scoring.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
18
|
+
[32m✓[39m dist/__tests__/language-filter.test.js [2m([22m[2m3 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
19
|
+
[32m✓[39m dist/__tests__/analyzer.test.js [2m([22m[2m18 tests[22m[2m)[22m[32m 190[2mms[22m[39m
|
|
20
|
+
[32m✓[39m src/__tests__/analyzer.test.ts [2m([22m[2m18 tests[22m[2m)[22m[32m 236[2mms[22m[39m
|
|
23
21
|
|
|
24
22
|
[2m Test Files [22m [1m[32m8 passed[39m[22m[90m (8)[39m
|
|
25
23
|
[2m Tests [22m [1m[32m61 passed[39m[22m[90m (61)[39m
|
|
26
|
-
[2m Start at [22m
|
|
27
|
-
[2m Duration [22m
|
|
24
|
+
[2m Start at [22m 10:36:28
|
|
25
|
+
[2m Duration [22m 1.30s[2m (transform 1.29s, setup 0ms, import 4.87s, tests 458ms, environment 1ms)[22m
|
|
28
26
|
|
|
29
27
|
[?25h
|
|
@@ -0,0 +1,731 @@
|
|
|
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, 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 styleStats = {
|
|
440
|
+
tryCatch: 0,
|
|
441
|
+
thenCatch: 0,
|
|
442
|
+
asyncAwait: 0,
|
|
443
|
+
commonJs: 0,
|
|
444
|
+
esm: 0
|
|
445
|
+
};
|
|
446
|
+
for (const filePath of filePaths) {
|
|
447
|
+
try {
|
|
448
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
449
|
+
contents.set(filePath, content);
|
|
450
|
+
if (content.match(tryCatchPattern)) styleStats.tryCatch++;
|
|
451
|
+
if (content.match(/\.catch\s*\(/)) styleStats.thenCatch++;
|
|
452
|
+
if (content.match(/\bawait\b/)) styleStats.asyncAwait++;
|
|
453
|
+
if (content.match(/\brequire\s*\(/)) styleStats.commonJs++;
|
|
454
|
+
if (content.match(/\bimport\b.*\bfrom\b/)) styleStats.esm++;
|
|
455
|
+
} catch (err) {
|
|
456
|
+
void err;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (styleStats.tryCatch > 0 && styleStats.thenCatch > 0) {
|
|
460
|
+
const dominant = styleStats.tryCatch >= styleStats.thenCatch ? "try-catch" : ".catch()";
|
|
461
|
+
const minority = dominant === "try-catch" ? ".catch()" : "try-catch";
|
|
462
|
+
issues.push({
|
|
463
|
+
files: filePaths.filter((f) => {
|
|
464
|
+
const c = contents.get(f) || "";
|
|
465
|
+
return minority === "try-catch" ? c.match(tryCatchPattern) : c.match(/\.catch\s*\(/);
|
|
466
|
+
}),
|
|
467
|
+
type: "pattern-inconsistency",
|
|
468
|
+
description: `Mixed error handling styles: codebase primarily uses ${dominant}, but found ${minority} in some files.`,
|
|
469
|
+
examples: [dominant, minority],
|
|
470
|
+
severity: Severity3.Minor
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
if (styleStats.commonJs > 0 && styleStats.esm > 0) {
|
|
474
|
+
const minority = styleStats.esm >= styleStats.commonJs ? "CommonJS (require)" : "ESM (import)";
|
|
475
|
+
issues.push({
|
|
476
|
+
files: filePaths.filter((f) => {
|
|
477
|
+
const c = contents.get(f) || "";
|
|
478
|
+
return minority === "CommonJS (require)" ? c.match(/\brequire\s*\(/) : c.match(/\bimport\b/);
|
|
479
|
+
}),
|
|
480
|
+
type: "pattern-inconsistency",
|
|
481
|
+
description: `Mixed module systems: found both ESM and CommonJS.`,
|
|
482
|
+
examples: ['import X from "y"', 'const X = require("y")'],
|
|
483
|
+
severity: Severity3.Major
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
return issues;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/analyzer.ts
|
|
490
|
+
import {
|
|
491
|
+
scanFiles,
|
|
492
|
+
Severity as Severity5,
|
|
493
|
+
IssueType,
|
|
494
|
+
getSeverityLevel
|
|
495
|
+
} from "@aiready/core";
|
|
496
|
+
|
|
497
|
+
// src/analyzers/naming-generalized.ts
|
|
498
|
+
import { getParser, Severity as Severity4 } from "@aiready/core";
|
|
499
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
500
|
+
async function analyzeNamingGeneralized(files) {
|
|
501
|
+
const issues = [];
|
|
502
|
+
for (const file of files) {
|
|
503
|
+
const parser = getParser(file);
|
|
504
|
+
if (!parser) continue;
|
|
505
|
+
try {
|
|
506
|
+
const code = readFileSync3(file, "utf-8");
|
|
507
|
+
await parser.initialize();
|
|
508
|
+
const result = parser.parse(code, file);
|
|
509
|
+
const conventions = parser.getNamingConventions();
|
|
510
|
+
for (const exp of result.exports) {
|
|
511
|
+
let pattern;
|
|
512
|
+
if (exp.type === "class") {
|
|
513
|
+
pattern = conventions.classPattern;
|
|
514
|
+
} else if (exp.type === "interface" && conventions.interfacePattern) {
|
|
515
|
+
pattern = conventions.interfacePattern;
|
|
516
|
+
} else if (exp.type === "type" && conventions.typePattern) {
|
|
517
|
+
pattern = conventions.typePattern;
|
|
518
|
+
} else if (exp.type === "function") {
|
|
519
|
+
pattern = conventions.functionPattern;
|
|
520
|
+
} else if (exp.type === "const") {
|
|
521
|
+
pattern = conventions.constantPattern;
|
|
522
|
+
} else {
|
|
523
|
+
pattern = conventions.variablePattern;
|
|
524
|
+
}
|
|
525
|
+
if (pattern && !pattern.test(exp.name)) {
|
|
526
|
+
issues.push({
|
|
527
|
+
type: "naming-inconsistency",
|
|
528
|
+
identifier: exp.name,
|
|
529
|
+
file,
|
|
530
|
+
line: exp.loc?.start.line || 1,
|
|
531
|
+
column: exp.loc?.start.column || 0,
|
|
532
|
+
severity: Severity4.Major,
|
|
533
|
+
category: "naming",
|
|
534
|
+
suggestion: `Follow ${parser.language} ${exp.type} naming convention: ${pattern.toString()}`
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
for (const imp of result.imports) {
|
|
539
|
+
for (const spec of imp.specifiers) {
|
|
540
|
+
if (spec === "*" || spec === "default") continue;
|
|
541
|
+
if (!conventions.variablePattern.test(spec) && !conventions.classPattern.test(spec)) {
|
|
542
|
+
issues.push({
|
|
543
|
+
type: "naming-inconsistency",
|
|
544
|
+
identifier: spec,
|
|
545
|
+
file,
|
|
546
|
+
line: imp.loc?.start.line || 1,
|
|
547
|
+
column: imp.loc?.start.column || 0,
|
|
548
|
+
severity: Severity4.Minor,
|
|
549
|
+
category: "naming",
|
|
550
|
+
suggestion: `Imported identifier '${spec}' may not follow standard conventions for this language.`
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
} catch (error) {
|
|
556
|
+
console.warn(`Consistency: Failed to analyze ${file}: ${error}`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return issues;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// src/analyzer.ts
|
|
563
|
+
async function analyzeConsistency(options) {
|
|
564
|
+
const {
|
|
565
|
+
checkNaming = true,
|
|
566
|
+
checkPatterns = true,
|
|
567
|
+
checkArchitecture = false,
|
|
568
|
+
// Not implemented yet
|
|
569
|
+
minSeverity = Severity5.Info,
|
|
570
|
+
...scanOptions
|
|
571
|
+
} = options;
|
|
572
|
+
void checkArchitecture;
|
|
573
|
+
const filePaths = await scanFiles(scanOptions);
|
|
574
|
+
let namingIssues = [];
|
|
575
|
+
if (checkNaming) {
|
|
576
|
+
namingIssues = await analyzeNamingGeneralized(filePaths);
|
|
577
|
+
const tsJsFiles = filePaths.filter((f) => /\.(ts|tsx|js|jsx)$/i.test(f));
|
|
578
|
+
if (tsJsFiles.length > 0) {
|
|
579
|
+
const deepTsIssues = await analyzeNamingAST(tsJsFiles);
|
|
580
|
+
namingIssues = [...namingIssues, ...deepTsIssues];
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const patternIssues = checkPatterns ? await analyzePatterns(filePaths) : [];
|
|
584
|
+
const results = [];
|
|
585
|
+
const fileIssuesMap = /* @__PURE__ */ new Map();
|
|
586
|
+
for (const issue of namingIssues) {
|
|
587
|
+
if (!shouldIncludeSeverity(issue.severity, minSeverity)) continue;
|
|
588
|
+
const fileName = issue.fileName || issue.file || issue.filePath || "unknown";
|
|
589
|
+
if (!fileIssuesMap.has(fileName)) fileIssuesMap.set(fileName, []);
|
|
590
|
+
fileIssuesMap.get(fileName).push(issue);
|
|
591
|
+
}
|
|
592
|
+
for (const issue of patternIssues) {
|
|
593
|
+
if (!shouldIncludeSeverity(issue.severity, minSeverity)) continue;
|
|
594
|
+
const fileName = issue.fileName || issue.file || issue.filePath || (Array.isArray(issue.files) ? issue.files[0] : "unknown");
|
|
595
|
+
if (!fileIssuesMap.has(fileName)) fileIssuesMap.set(fileName, []);
|
|
596
|
+
fileIssuesMap.get(fileName).push(issue);
|
|
597
|
+
}
|
|
598
|
+
for (const [fileName, issues] of fileIssuesMap.entries()) {
|
|
599
|
+
results.push({
|
|
600
|
+
fileName,
|
|
601
|
+
issues: issues.map((i) => transformToIssue(i)),
|
|
602
|
+
metrics: {
|
|
603
|
+
consistencyScore: calculateConsistencyScore(issues)
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
const recommendations = [];
|
|
608
|
+
if (namingIssues.length > 0) {
|
|
609
|
+
recommendations.push("Standardize naming conventions across the codebase");
|
|
610
|
+
}
|
|
611
|
+
if (patternIssues.length > 0) {
|
|
612
|
+
recommendations.push("Consolidate repetitive implementation patterns");
|
|
613
|
+
}
|
|
614
|
+
if (results.some((r) => (r.metrics?.consistencyScore ?? 1) < 0.8)) {
|
|
615
|
+
recommendations.push(
|
|
616
|
+
"Improve cross-module consistency to reduce AI confusion"
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
results,
|
|
621
|
+
summary: {
|
|
622
|
+
filesAnalyzed: filePaths.length,
|
|
623
|
+
totalIssues: results.reduce((acc, r) => acc + r.issues.length, 0),
|
|
624
|
+
namingIssues: namingIssues.length,
|
|
625
|
+
patternIssues: patternIssues.length,
|
|
626
|
+
architectureIssues: 0
|
|
627
|
+
},
|
|
628
|
+
recommendations,
|
|
629
|
+
metadata: {
|
|
630
|
+
toolName: "naming-consistency",
|
|
631
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
function shouldIncludeSeverity(severity, minSeverity) {
|
|
636
|
+
return getSeverityLevel(severity) >= getSeverityLevel(minSeverity);
|
|
637
|
+
}
|
|
638
|
+
function getIssueType(type) {
|
|
639
|
+
if (!type) return IssueType.NamingInconsistency;
|
|
640
|
+
const typeMap = {
|
|
641
|
+
"naming-inconsistency": IssueType.NamingInconsistency,
|
|
642
|
+
"naming-quality": IssueType.NamingQuality,
|
|
643
|
+
"pattern-inconsistency": IssueType.PatternInconsistency,
|
|
644
|
+
"architecture-inconsistency": IssueType.ArchitectureInconsistency,
|
|
645
|
+
"error-handling": IssueType.PatternInconsistency,
|
|
646
|
+
"async-style": IssueType.PatternInconsistency,
|
|
647
|
+
"import-style": IssueType.PatternInconsistency,
|
|
648
|
+
"api-design": IssueType.PatternInconsistency
|
|
649
|
+
};
|
|
650
|
+
return typeMap[type] || IssueType.NamingInconsistency;
|
|
651
|
+
}
|
|
652
|
+
function transformToIssue(i) {
|
|
653
|
+
if (i.message && i.location) {
|
|
654
|
+
return {
|
|
655
|
+
type: getIssueType(i.type),
|
|
656
|
+
severity: i.severity,
|
|
657
|
+
message: i.message,
|
|
658
|
+
location: i.location,
|
|
659
|
+
suggestion: i.suggestion
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
if (i.identifier || i.type) {
|
|
663
|
+
const line = i.line || 1;
|
|
664
|
+
const column = i.column || 1;
|
|
665
|
+
return {
|
|
666
|
+
type: getIssueType(i.type),
|
|
667
|
+
severity: i.severity,
|
|
668
|
+
message: i.suggestion ? `Naming issue: ${i.suggestion}` : `Naming issue for '${i.identifier || "unknown"}'`,
|
|
669
|
+
location: {
|
|
670
|
+
file: i.file || i.fileName || "",
|
|
671
|
+
line,
|
|
672
|
+
column,
|
|
673
|
+
endLine: line,
|
|
674
|
+
endColumn: column + (i.identifier?.length || 10)
|
|
675
|
+
},
|
|
676
|
+
suggestion: i.suggestion
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
if (i.description || i.files) {
|
|
680
|
+
const fileName = Array.isArray(i.files) ? i.files[0] : i.file || "";
|
|
681
|
+
return {
|
|
682
|
+
type: getIssueType(i.type),
|
|
683
|
+
severity: i.severity,
|
|
684
|
+
message: i.description || "Pattern inconsistency found",
|
|
685
|
+
location: {
|
|
686
|
+
file: fileName,
|
|
687
|
+
line: 1,
|
|
688
|
+
column: 1,
|
|
689
|
+
endLine: 1,
|
|
690
|
+
endColumn: 10
|
|
691
|
+
},
|
|
692
|
+
suggestion: i.examples?.[0]
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
type: getIssueType(i.type),
|
|
697
|
+
severity: i.severity,
|
|
698
|
+
message: i.message || "Unknown issue",
|
|
699
|
+
location: i.location || { file: "", line: 1, column: 1 },
|
|
700
|
+
suggestion: i.suggestion
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
function calculateConsistencyScore(issues) {
|
|
704
|
+
let totalWeight = 0;
|
|
705
|
+
for (const issue of issues) {
|
|
706
|
+
const val = getSeverityLevel(issue.severity);
|
|
707
|
+
switch (val) {
|
|
708
|
+
case 4:
|
|
709
|
+
totalWeight += 10;
|
|
710
|
+
break;
|
|
711
|
+
case 3:
|
|
712
|
+
totalWeight += 5;
|
|
713
|
+
break;
|
|
714
|
+
case 2:
|
|
715
|
+
totalWeight += 2;
|
|
716
|
+
break;
|
|
717
|
+
case 1:
|
|
718
|
+
totalWeight += 1;
|
|
719
|
+
break;
|
|
720
|
+
default:
|
|
721
|
+
totalWeight += 1;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return Math.max(0, 1 - totalWeight / 100);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
export {
|
|
728
|
+
analyzeNamingAST,
|
|
729
|
+
analyzePatterns,
|
|
730
|
+
analyzeConsistency
|
|
731
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -635,7 +635,9 @@ async function analyzeConsistency(options) {
|
|
|
635
635
|
recommendations.push("Consolidate repetitive implementation patterns");
|
|
636
636
|
}
|
|
637
637
|
if (results.some((r) => (r.metrics?.consistencyScore ?? 1) < 0.8)) {
|
|
638
|
-
recommendations.push(
|
|
638
|
+
recommendations.push(
|
|
639
|
+
"Improve cross-module consistency to reduce AI confusion"
|
|
640
|
+
);
|
|
639
641
|
}
|
|
640
642
|
return {
|
|
641
643
|
results,
|
package/dist/cli.mjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -643,7 +643,9 @@ async function analyzeConsistency(options) {
|
|
|
643
643
|
recommendations.push("Consolidate repetitive implementation patterns");
|
|
644
644
|
}
|
|
645
645
|
if (results.some((r) => (r.metrics?.consistencyScore ?? 1) < 0.8)) {
|
|
646
|
-
recommendations.push(
|
|
646
|
+
recommendations.push(
|
|
647
|
+
"Improve cross-module consistency to reduce AI confusion"
|
|
648
|
+
);
|
|
647
649
|
}
|
|
648
650
|
return {
|
|
649
651
|
results,
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/consistency",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.4",
|
|
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.23.
|
|
46
|
+
"@aiready/core": "0.23.4"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/node": "^24.0.0",
|
package/src/analyzer.ts
CHANGED
|
@@ -104,7 +104,9 @@ export async function analyzeConsistency(
|
|
|
104
104
|
recommendations.push('Consolidate repetitive implementation patterns');
|
|
105
105
|
}
|
|
106
106
|
if (results.some((r) => (r.metrics?.consistencyScore ?? 1) < 0.8)) {
|
|
107
|
-
recommendations.push(
|
|
107
|
+
recommendations.push(
|
|
108
|
+
'Improve cross-module consistency to reduce AI confusion'
|
|
109
|
+
);
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
return {
|