@aiready/contract-enforcement 0.2.5 → 0.2.7
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 +19 -18
- package/.turbo/turbo-test.log +18 -16
- package/dist/index.js +71 -44
- package/dist/index.mjs +71 -44
- package/package.json +2 -2
- package/src/__tests__/detector.test.ts +13 -1
- package/src/detector.ts +98 -61
- package/src/provider.ts +16 -3
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
DTS
|
|
17
|
-
DTS
|
|
18
|
-
DTS dist/index.d.
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
> @aiready/contract-enforcement@0.2.7 build /Users/pengcao/projects/aiready/packages/contract-enforcement
|
|
4
|
+
> tsup src/index.ts --format cjs,esm --dts
|
|
5
|
+
|
|
6
|
+
[34mCLI[39m Building entry: src/index.ts
|
|
7
|
+
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
8
|
+
[34mCLI[39m tsup v8.5.1
|
|
9
|
+
[34mCLI[39m Target: es2020
|
|
10
|
+
[34mCJS[39m Build start
|
|
11
|
+
[34mESM[39m Build start
|
|
12
|
+
[32mCJS[39m [1mdist/index.js [22m[32m18.88 KB[39m
|
|
13
|
+
[32mCJS[39m ⚡️ Build success in 64ms
|
|
14
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m17.44 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 64ms
|
|
16
|
+
DTS Build start
|
|
17
|
+
DTS ⚡️ Build success in 9740ms
|
|
18
|
+
DTS dist/index.d.ts 2.45 KB
|
|
19
|
+
DTS dist/index.d.mts 2.45 KB
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
[
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
[32m✓[39m src/__tests__/
|
|
10
|
-
[32m✓[39m src/__tests__/provider.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
[2m
|
|
14
|
-
[2m
|
|
15
|
-
[2m
|
|
16
|
-
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
> @aiready/contract-enforcement@0.2.6 test /Users/pengcao/projects/aiready/packages/contract-enforcement
|
|
4
|
+
> vitest run
|
|
5
|
+
|
|
6
|
+
[?25l
|
|
7
|
+
[1m[46m RUN [49m[22m [36mv4.1.1 [39m[90m/Users/pengcao/projects/aiready/packages/contract-enforcement[39m
|
|
8
|
+
|
|
9
|
+
[32m✓[39m src/__tests__/scoring.test.ts [2m([22m[2m7 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
10
|
+
[32m✓[39m src/__tests__/provider.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
11
|
+
[32m✓[39m src/__tests__/detector.test.ts [2m([22m[2m15 tests[22m[2m)[22m[32m 47[2mms[22m[39m
|
|
12
|
+
|
|
13
|
+
[2m Test Files [22m [1m[32m3 passed[39m[22m[90m (3)[39m
|
|
14
|
+
[2m Tests [22m [1m[32m27 passed[39m[22m[90m (27)[39m
|
|
15
|
+
[2m Start at [22m 22:04:34
|
|
16
|
+
[2m Duration [22m 1.50s[2m (transform 687ms, setup 0ms, import 2.52s, tests 56ms, environment 0ms)[22m
|
|
17
|
+
|
|
18
|
+
[?25h
|
package/dist/index.js
CHANGED
|
@@ -109,7 +109,6 @@ function countOptionalChainDepth(node) {
|
|
|
109
109
|
return depth;
|
|
110
110
|
}
|
|
111
111
|
function isLiteral(node) {
|
|
112
|
-
if (!node) return false;
|
|
113
112
|
if (node.type === "Literal") return true;
|
|
114
113
|
if (node.type === "TemplateLiteral" && node.expressions.length === 0)
|
|
115
114
|
return true;
|
|
@@ -119,10 +118,9 @@ function isLiteral(node) {
|
|
|
119
118
|
return false;
|
|
120
119
|
}
|
|
121
120
|
function isProcessEnvAccess(node) {
|
|
122
|
-
return node?.type === "MemberExpression" && node.object?.type === "MemberExpression" && node.object.object?.name === "process" && node.object.property?.name === "env";
|
|
121
|
+
return node?.type === "MemberExpression" && node.object?.type === "MemberExpression" && node.object.object?.type === "Identifier" && node.object.object.name === "process" && node.object.property?.type === "Identifier" && node.object.property.name === "env";
|
|
123
122
|
}
|
|
124
123
|
function isSstResourceAccess(node) {
|
|
125
|
-
if (!node) return false;
|
|
126
124
|
let current = node;
|
|
127
125
|
if (current.type === "ChainExpression") {
|
|
128
126
|
current = current.expression;
|
|
@@ -145,9 +143,16 @@ function isSwallowedCatch(body, filePath) {
|
|
|
145
143
|
const stmt = body[0];
|
|
146
144
|
if (stmt.type === "ExpressionStatement" && stmt.expression?.type === "CallExpression") {
|
|
147
145
|
const callee = stmt.expression.callee;
|
|
148
|
-
if (callee?.object?.name === "console")
|
|
146
|
+
if (callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && callee.object.name === "console") {
|
|
147
|
+
const method = callee.property.type === "Identifier" ? callee.property.name : "";
|
|
148
|
+
if (method === "error" || method === "warn") return false;
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
149
151
|
if (isUiComponent) {
|
|
150
|
-
|
|
152
|
+
let calleeName = "";
|
|
153
|
+
if (callee?.type === "Identifier") calleeName = callee.name;
|
|
154
|
+
else if (callee?.type === "MemberExpression" && callee.property.type === "Identifier")
|
|
155
|
+
calleeName = callee.property.name;
|
|
151
156
|
if (/telemetry|analytics|track|logEvent/i.test(calleeName)) {
|
|
152
157
|
return false;
|
|
153
158
|
}
|
|
@@ -193,9 +198,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
193
198
|
import_core.Severity.Major,
|
|
194
199
|
"`as any` type assertion bypasses type safety",
|
|
195
200
|
filePath,
|
|
196
|
-
node.loc
|
|
197
|
-
node.loc
|
|
198
|
-
getLineContent(code, node.loc
|
|
201
|
+
node.loc.start.line,
|
|
202
|
+
node.loc.start.column,
|
|
203
|
+
getLineContent(code, node.loc.start.line)
|
|
199
204
|
)
|
|
200
205
|
);
|
|
201
206
|
}
|
|
@@ -209,9 +214,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
209
214
|
import_core.Severity.Major,
|
|
210
215
|
"`as unknown` double-cast bypasses type safety",
|
|
211
216
|
filePath,
|
|
212
|
-
node.loc
|
|
213
|
-
node.loc
|
|
214
|
-
getLineContent(code, node.loc
|
|
217
|
+
node.loc.start.line,
|
|
218
|
+
node.loc.start.column,
|
|
219
|
+
getLineContent(code, node.loc.start.line)
|
|
215
220
|
)
|
|
216
221
|
);
|
|
217
222
|
}
|
|
@@ -226,9 +231,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
226
231
|
import_core.Severity.Minor,
|
|
227
232
|
`Optional chain depth of ${depth} indicates missing structural guarantees`,
|
|
228
233
|
filePath,
|
|
229
|
-
node.loc
|
|
230
|
-
node.loc
|
|
231
|
-
getLineContent(code, node.loc
|
|
234
|
+
node.loc.start.line,
|
|
235
|
+
node.loc.start.column,
|
|
236
|
+
getLineContent(code, node.loc.start.line)
|
|
232
237
|
)
|
|
233
238
|
);
|
|
234
239
|
}
|
|
@@ -242,9 +247,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
242
247
|
import_core.Severity.Minor,
|
|
243
248
|
"Nullish coalescing with literal default suggests missing upstream type guarantee",
|
|
244
249
|
filePath,
|
|
245
|
-
node.loc
|
|
246
|
-
node.loc
|
|
247
|
-
getLineContent(code, node.loc
|
|
250
|
+
node.loc.start.line,
|
|
251
|
+
node.loc.start.column,
|
|
252
|
+
getLineContent(code, node.loc.start.line)
|
|
248
253
|
)
|
|
249
254
|
);
|
|
250
255
|
}
|
|
@@ -259,9 +264,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
259
264
|
import_core.Severity.Major,
|
|
260
265
|
"Error is swallowed in catch block \u2014 failures will be silent",
|
|
261
266
|
filePath,
|
|
262
|
-
node.handler.loc
|
|
263
|
-
node.handler.loc
|
|
264
|
-
getLineContent(code, node.handler.loc
|
|
267
|
+
node.handler.loc.start.line,
|
|
268
|
+
node.handler.loc.start.column,
|
|
269
|
+
getLineContent(code, node.handler.loc.start.line)
|
|
265
270
|
)
|
|
266
271
|
);
|
|
267
272
|
}
|
|
@@ -274,9 +279,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
274
279
|
import_core.Severity.Minor,
|
|
275
280
|
"Environment variable with fallback \u2014 use a validated env schema instead",
|
|
276
281
|
filePath,
|
|
277
|
-
node.loc
|
|
278
|
-
node.loc
|
|
279
|
-
getLineContent(code, node.loc
|
|
282
|
+
node.loc.start.line,
|
|
283
|
+
node.loc.start.column,
|
|
284
|
+
getLineContent(code, node.loc.start.line)
|
|
280
285
|
)
|
|
281
286
|
);
|
|
282
287
|
}
|
|
@@ -296,16 +301,19 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
296
301
|
import_core.Severity.Info,
|
|
297
302
|
"Guard clause could be eliminated with non-nullable type at source",
|
|
298
303
|
filePath,
|
|
299
|
-
node.loc
|
|
300
|
-
node.loc
|
|
301
|
-
getLineContent(code, node.loc
|
|
304
|
+
node.loc.start.line,
|
|
305
|
+
node.loc.start.column,
|
|
306
|
+
getLineContent(code, node.loc.start.line)
|
|
302
307
|
)
|
|
303
308
|
);
|
|
304
309
|
}
|
|
305
310
|
}
|
|
306
311
|
if ((node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") && node.params) {
|
|
307
312
|
for (const param of node.params) {
|
|
308
|
-
|
|
313
|
+
let typeAnno;
|
|
314
|
+
if ("typeAnnotation" in param && param.typeAnnotation) {
|
|
315
|
+
typeAnno = param.typeAnnotation.typeAnnotation;
|
|
316
|
+
}
|
|
309
317
|
if (typeAnno?.type === "TSAnyKeyword") {
|
|
310
318
|
counts["any-parameter"]++;
|
|
311
319
|
issues.push(
|
|
@@ -314,27 +322,33 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
314
322
|
import_core.Severity.Major,
|
|
315
323
|
"Parameter typed as `any` bypasses type safety",
|
|
316
324
|
filePath,
|
|
317
|
-
param.loc
|
|
318
|
-
param.loc
|
|
319
|
-
getLineContent(code, param.loc
|
|
325
|
+
param.loc.start.line,
|
|
326
|
+
param.loc.start.column,
|
|
327
|
+
getLineContent(code, param.loc.start.line)
|
|
320
328
|
)
|
|
321
329
|
);
|
|
322
330
|
}
|
|
323
331
|
}
|
|
324
|
-
|
|
332
|
+
let returnAnno;
|
|
333
|
+
if ("returnType" in node && node.returnType) {
|
|
334
|
+
returnAnno = node.returnType.typeAnnotation;
|
|
335
|
+
}
|
|
325
336
|
if (returnAnno?.type === "TSAnyKeyword") {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
337
|
+
const fnNode = node;
|
|
338
|
+
if (fnNode.returnType?.loc) {
|
|
339
|
+
counts["any-return"]++;
|
|
340
|
+
issues.push(
|
|
341
|
+
makeIssue(
|
|
342
|
+
"any-return",
|
|
343
|
+
import_core.Severity.Major,
|
|
344
|
+
"Return type is `any` \u2014 callers cannot rely on the result shape",
|
|
345
|
+
filePath,
|
|
346
|
+
fnNode.returnType.loc.start.line,
|
|
347
|
+
fnNode.returnType.loc.start.column,
|
|
348
|
+
getLineContent(code, fnNode.returnType.loc.start.line)
|
|
349
|
+
)
|
|
350
|
+
);
|
|
351
|
+
}
|
|
338
352
|
}
|
|
339
353
|
}
|
|
340
354
|
for (const key in node) {
|
|
@@ -495,11 +509,24 @@ var ContractEnforcementProvider = (0, import_core3.createProvider)({
|
|
|
495
509
|
},
|
|
496
510
|
score(output, _options) {
|
|
497
511
|
const rawData = output.metadata?.rawData ?? {};
|
|
498
|
-
|
|
512
|
+
const result = calculateContractEnforcementScore(
|
|
499
513
|
rawData,
|
|
500
514
|
rawData.totalLines ?? 1,
|
|
501
515
|
rawData.sourceFiles ?? 1
|
|
502
516
|
);
|
|
517
|
+
return {
|
|
518
|
+
toolName: import_core3.ToolName.ContractEnforcement,
|
|
519
|
+
score: result.score,
|
|
520
|
+
rating: result.rating,
|
|
521
|
+
recommendations: result.recommendations.map((action) => ({
|
|
522
|
+
action,
|
|
523
|
+
estimatedImpact: 10,
|
|
524
|
+
priority: "medium"
|
|
525
|
+
})),
|
|
526
|
+
dimensions: result.dimensions,
|
|
527
|
+
rawMetrics: rawData,
|
|
528
|
+
factors: []
|
|
529
|
+
};
|
|
503
530
|
}
|
|
504
531
|
});
|
|
505
532
|
|
package/dist/index.mjs
CHANGED
|
@@ -82,7 +82,6 @@ function countOptionalChainDepth(node) {
|
|
|
82
82
|
return depth;
|
|
83
83
|
}
|
|
84
84
|
function isLiteral(node) {
|
|
85
|
-
if (!node) return false;
|
|
86
85
|
if (node.type === "Literal") return true;
|
|
87
86
|
if (node.type === "TemplateLiteral" && node.expressions.length === 0)
|
|
88
87
|
return true;
|
|
@@ -92,10 +91,9 @@ function isLiteral(node) {
|
|
|
92
91
|
return false;
|
|
93
92
|
}
|
|
94
93
|
function isProcessEnvAccess(node) {
|
|
95
|
-
return node?.type === "MemberExpression" && node.object?.type === "MemberExpression" && node.object.object?.name === "process" && node.object.property?.name === "env";
|
|
94
|
+
return node?.type === "MemberExpression" && node.object?.type === "MemberExpression" && node.object.object?.type === "Identifier" && node.object.object.name === "process" && node.object.property?.type === "Identifier" && node.object.property.name === "env";
|
|
96
95
|
}
|
|
97
96
|
function isSstResourceAccess(node) {
|
|
98
|
-
if (!node) return false;
|
|
99
97
|
let current = node;
|
|
100
98
|
if (current.type === "ChainExpression") {
|
|
101
99
|
current = current.expression;
|
|
@@ -118,9 +116,16 @@ function isSwallowedCatch(body, filePath) {
|
|
|
118
116
|
const stmt = body[0];
|
|
119
117
|
if (stmt.type === "ExpressionStatement" && stmt.expression?.type === "CallExpression") {
|
|
120
118
|
const callee = stmt.expression.callee;
|
|
121
|
-
if (callee?.object?.name === "console")
|
|
119
|
+
if (callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && callee.object.name === "console") {
|
|
120
|
+
const method = callee.property.type === "Identifier" ? callee.property.name : "";
|
|
121
|
+
if (method === "error" || method === "warn") return false;
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
122
124
|
if (isUiComponent) {
|
|
123
|
-
|
|
125
|
+
let calleeName = "";
|
|
126
|
+
if (callee?.type === "Identifier") calleeName = callee.name;
|
|
127
|
+
else if (callee?.type === "MemberExpression" && callee.property.type === "Identifier")
|
|
128
|
+
calleeName = callee.property.name;
|
|
124
129
|
if (/telemetry|analytics|track|logEvent/i.test(calleeName)) {
|
|
125
130
|
return false;
|
|
126
131
|
}
|
|
@@ -166,9 +171,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
166
171
|
Severity.Major,
|
|
167
172
|
"`as any` type assertion bypasses type safety",
|
|
168
173
|
filePath,
|
|
169
|
-
node.loc
|
|
170
|
-
node.loc
|
|
171
|
-
getLineContent(code, node.loc
|
|
174
|
+
node.loc.start.line,
|
|
175
|
+
node.loc.start.column,
|
|
176
|
+
getLineContent(code, node.loc.start.line)
|
|
172
177
|
)
|
|
173
178
|
);
|
|
174
179
|
}
|
|
@@ -182,9 +187,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
182
187
|
Severity.Major,
|
|
183
188
|
"`as unknown` double-cast bypasses type safety",
|
|
184
189
|
filePath,
|
|
185
|
-
node.loc
|
|
186
|
-
node.loc
|
|
187
|
-
getLineContent(code, node.loc
|
|
190
|
+
node.loc.start.line,
|
|
191
|
+
node.loc.start.column,
|
|
192
|
+
getLineContent(code, node.loc.start.line)
|
|
188
193
|
)
|
|
189
194
|
);
|
|
190
195
|
}
|
|
@@ -199,9 +204,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
199
204
|
Severity.Minor,
|
|
200
205
|
`Optional chain depth of ${depth} indicates missing structural guarantees`,
|
|
201
206
|
filePath,
|
|
202
|
-
node.loc
|
|
203
|
-
node.loc
|
|
204
|
-
getLineContent(code, node.loc
|
|
207
|
+
node.loc.start.line,
|
|
208
|
+
node.loc.start.column,
|
|
209
|
+
getLineContent(code, node.loc.start.line)
|
|
205
210
|
)
|
|
206
211
|
);
|
|
207
212
|
}
|
|
@@ -215,9 +220,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
215
220
|
Severity.Minor,
|
|
216
221
|
"Nullish coalescing with literal default suggests missing upstream type guarantee",
|
|
217
222
|
filePath,
|
|
218
|
-
node.loc
|
|
219
|
-
node.loc
|
|
220
|
-
getLineContent(code, node.loc
|
|
223
|
+
node.loc.start.line,
|
|
224
|
+
node.loc.start.column,
|
|
225
|
+
getLineContent(code, node.loc.start.line)
|
|
221
226
|
)
|
|
222
227
|
);
|
|
223
228
|
}
|
|
@@ -232,9 +237,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
232
237
|
Severity.Major,
|
|
233
238
|
"Error is swallowed in catch block \u2014 failures will be silent",
|
|
234
239
|
filePath,
|
|
235
|
-
node.handler.loc
|
|
236
|
-
node.handler.loc
|
|
237
|
-
getLineContent(code, node.handler.loc
|
|
240
|
+
node.handler.loc.start.line,
|
|
241
|
+
node.handler.loc.start.column,
|
|
242
|
+
getLineContent(code, node.handler.loc.start.line)
|
|
238
243
|
)
|
|
239
244
|
);
|
|
240
245
|
}
|
|
@@ -247,9 +252,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
247
252
|
Severity.Minor,
|
|
248
253
|
"Environment variable with fallback \u2014 use a validated env schema instead",
|
|
249
254
|
filePath,
|
|
250
|
-
node.loc
|
|
251
|
-
node.loc
|
|
252
|
-
getLineContent(code, node.loc
|
|
255
|
+
node.loc.start.line,
|
|
256
|
+
node.loc.start.column,
|
|
257
|
+
getLineContent(code, node.loc.start.line)
|
|
253
258
|
)
|
|
254
259
|
);
|
|
255
260
|
}
|
|
@@ -269,16 +274,19 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
269
274
|
Severity.Info,
|
|
270
275
|
"Guard clause could be eliminated with non-nullable type at source",
|
|
271
276
|
filePath,
|
|
272
|
-
node.loc
|
|
273
|
-
node.loc
|
|
274
|
-
getLineContent(code, node.loc
|
|
277
|
+
node.loc.start.line,
|
|
278
|
+
node.loc.start.column,
|
|
279
|
+
getLineContent(code, node.loc.start.line)
|
|
275
280
|
)
|
|
276
281
|
);
|
|
277
282
|
}
|
|
278
283
|
}
|
|
279
284
|
if ((node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") && node.params) {
|
|
280
285
|
for (const param of node.params) {
|
|
281
|
-
|
|
286
|
+
let typeAnno;
|
|
287
|
+
if ("typeAnnotation" in param && param.typeAnnotation) {
|
|
288
|
+
typeAnno = param.typeAnnotation.typeAnnotation;
|
|
289
|
+
}
|
|
282
290
|
if (typeAnno?.type === "TSAnyKeyword") {
|
|
283
291
|
counts["any-parameter"]++;
|
|
284
292
|
issues.push(
|
|
@@ -287,27 +295,33 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
287
295
|
Severity.Major,
|
|
288
296
|
"Parameter typed as `any` bypasses type safety",
|
|
289
297
|
filePath,
|
|
290
|
-
param.loc
|
|
291
|
-
param.loc
|
|
292
|
-
getLineContent(code, param.loc
|
|
298
|
+
param.loc.start.line,
|
|
299
|
+
param.loc.start.column,
|
|
300
|
+
getLineContent(code, param.loc.start.line)
|
|
293
301
|
)
|
|
294
302
|
);
|
|
295
303
|
}
|
|
296
304
|
}
|
|
297
|
-
|
|
305
|
+
let returnAnno;
|
|
306
|
+
if ("returnType" in node && node.returnType) {
|
|
307
|
+
returnAnno = node.returnType.typeAnnotation;
|
|
308
|
+
}
|
|
298
309
|
if (returnAnno?.type === "TSAnyKeyword") {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
310
|
+
const fnNode = node;
|
|
311
|
+
if (fnNode.returnType?.loc) {
|
|
312
|
+
counts["any-return"]++;
|
|
313
|
+
issues.push(
|
|
314
|
+
makeIssue(
|
|
315
|
+
"any-return",
|
|
316
|
+
Severity.Major,
|
|
317
|
+
"Return type is `any` \u2014 callers cannot rely on the result shape",
|
|
318
|
+
filePath,
|
|
319
|
+
fnNode.returnType.loc.start.line,
|
|
320
|
+
fnNode.returnType.loc.start.column,
|
|
321
|
+
getLineContent(code, fnNode.returnType.loc.start.line)
|
|
322
|
+
)
|
|
323
|
+
);
|
|
324
|
+
}
|
|
311
325
|
}
|
|
312
326
|
}
|
|
313
327
|
for (const key in node) {
|
|
@@ -468,11 +482,24 @@ var ContractEnforcementProvider = createProvider({
|
|
|
468
482
|
},
|
|
469
483
|
score(output, _options) {
|
|
470
484
|
const rawData = output.metadata?.rawData ?? {};
|
|
471
|
-
|
|
485
|
+
const result = calculateContractEnforcementScore(
|
|
472
486
|
rawData,
|
|
473
487
|
rawData.totalLines ?? 1,
|
|
474
488
|
rawData.sourceFiles ?? 1
|
|
475
489
|
);
|
|
490
|
+
return {
|
|
491
|
+
toolName: ToolName.ContractEnforcement,
|
|
492
|
+
score: result.score,
|
|
493
|
+
rating: result.rating,
|
|
494
|
+
recommendations: result.recommendations.map((action) => ({
|
|
495
|
+
action,
|
|
496
|
+
estimatedImpact: 10,
|
|
497
|
+
priority: "medium"
|
|
498
|
+
})),
|
|
499
|
+
dimensions: result.dimensions,
|
|
500
|
+
rawMetrics: rawData,
|
|
501
|
+
factors: []
|
|
502
|
+
};
|
|
476
503
|
}
|
|
477
504
|
});
|
|
478
505
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/contract-enforcement",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Measures structural type safety and boundary validation to reduce fallback cascades for AI agents",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"homepage": "https://github.com/caopengau/aiready-contract-enforcement",
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@typescript-eslint/typescript-estree": "^8.53.0",
|
|
35
|
-
"@aiready/core": "0.24.
|
|
35
|
+
"@aiready/core": "0.24.7"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^24.0.0",
|
|
@@ -72,13 +72,25 @@ describe('detectDefensivePatterns', () => {
|
|
|
72
72
|
try {
|
|
73
73
|
doSomething();
|
|
74
74
|
} catch (e) {
|
|
75
|
-
console.
|
|
75
|
+
console.log(e);
|
|
76
76
|
}
|
|
77
77
|
`;
|
|
78
78
|
const result = detectDefensivePatterns(filePath, code);
|
|
79
79
|
expect(result.counts['swallowed-error']).toBe(1);
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
+
it('does not flag console.error as swallowed', () => {
|
|
83
|
+
const code = `
|
|
84
|
+
try {
|
|
85
|
+
doSomething();
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.error(e);
|
|
88
|
+
}
|
|
89
|
+
`;
|
|
90
|
+
const result = detectDefensivePatterns(filePath, code);
|
|
91
|
+
expect(result.counts['swallowed-error']).toBe(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
82
94
|
it('does not flag catch blocks with real handling', () => {
|
|
83
95
|
const code = `
|
|
84
96
|
try {
|
package/src/detector.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parse } from '@typescript-eslint/typescript-estree';
|
|
1
|
+
import { parse, TSESTree } from '@typescript-eslint/typescript-estree';
|
|
2
2
|
import { Severity, IssueType } from '@aiready/core';
|
|
3
3
|
import type {
|
|
4
4
|
ContractEnforcementIssue,
|
|
@@ -56,9 +56,9 @@ function getLineContent(code: string, line: number): string {
|
|
|
56
56
|
return (lines[line - 1] || '').trim().slice(0, 120);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
function countOptionalChainDepth(node:
|
|
59
|
+
function countOptionalChainDepth(node: TSESTree.Node): number {
|
|
60
60
|
let depth = 0;
|
|
61
|
-
let current = node;
|
|
61
|
+
let current: TSESTree.Node | undefined = node;
|
|
62
62
|
while (current) {
|
|
63
63
|
if (current.type === 'MemberExpression' && current.optional) {
|
|
64
64
|
depth++;
|
|
@@ -75,8 +75,7 @@ function countOptionalChainDepth(node: any): number {
|
|
|
75
75
|
return depth;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
function isLiteral(node:
|
|
79
|
-
if (!node) return false;
|
|
78
|
+
function isLiteral(node: TSESTree.Node): boolean {
|
|
80
79
|
if (node.type === 'Literal') return true;
|
|
81
80
|
if (node.type === 'TemplateLiteral' && node.expressions.length === 0)
|
|
82
81
|
return true;
|
|
@@ -89,18 +88,19 @@ function isLiteral(node: any): boolean {
|
|
|
89
88
|
return false;
|
|
90
89
|
}
|
|
91
90
|
|
|
92
|
-
function isProcessEnvAccess(node:
|
|
91
|
+
function isProcessEnvAccess(node: TSESTree.Node | undefined): boolean {
|
|
93
92
|
return (
|
|
94
93
|
node?.type === 'MemberExpression' &&
|
|
95
94
|
node.object?.type === 'MemberExpression' &&
|
|
96
|
-
node.object.object?.
|
|
97
|
-
node.object.
|
|
95
|
+
node.object.object?.type === 'Identifier' &&
|
|
96
|
+
node.object.object.name === 'process' &&
|
|
97
|
+
node.object.property?.type === 'Identifier' &&
|
|
98
|
+
node.object.property.name === 'env'
|
|
98
99
|
);
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
function isSstResourceAccess(node:
|
|
102
|
-
|
|
103
|
-
let current = node;
|
|
102
|
+
function isSstResourceAccess(node: TSESTree.Node): boolean {
|
|
103
|
+
let current: TSESTree.Node = node;
|
|
104
104
|
// Handle ChainExpression for optional chaining like Resource.MySecret?.value
|
|
105
105
|
if (current.type === 'ChainExpression') {
|
|
106
106
|
current = current.expression;
|
|
@@ -125,7 +125,10 @@ function isSstResourceAccess(node: any): boolean {
|
|
|
125
125
|
return false;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
function isSwallowedCatch(
|
|
128
|
+
function isSwallowedCatch(
|
|
129
|
+
body: TSESTree.Statement[],
|
|
130
|
+
filePath: string
|
|
131
|
+
): boolean {
|
|
129
132
|
if (body.length === 0) return true;
|
|
130
133
|
|
|
131
134
|
// UI components often have intentional silent catches for telemetry/analytics
|
|
@@ -138,12 +141,28 @@ function isSwallowedCatch(body: any[], filePath: string): boolean {
|
|
|
138
141
|
stmt.expression?.type === 'CallExpression'
|
|
139
142
|
) {
|
|
140
143
|
const callee = stmt.expression.callee;
|
|
141
|
-
// console.
|
|
142
|
-
if (
|
|
144
|
+
// console.error and console.warn are considered "handling" or at least "reporting"
|
|
145
|
+
if (
|
|
146
|
+
callee?.type === 'MemberExpression' &&
|
|
147
|
+
callee.object?.type === 'Identifier' &&
|
|
148
|
+
callee.object.name === 'console'
|
|
149
|
+
) {
|
|
150
|
+
const method =
|
|
151
|
+
callee.property.type === 'Identifier' ? callee.property.name : '';
|
|
152
|
+
if (method === 'error' || method === 'warn') return false;
|
|
153
|
+
return true; // console.log/info/debug etc. are still considered "swallowed"
|
|
154
|
+
}
|
|
143
155
|
|
|
144
156
|
// If it's a UI component and looks like telemetry, it's a false positive
|
|
145
157
|
if (isUiComponent) {
|
|
146
|
-
|
|
158
|
+
let calleeName = '';
|
|
159
|
+
if (callee?.type === 'Identifier') calleeName = callee.name;
|
|
160
|
+
else if (
|
|
161
|
+
callee?.type === 'MemberExpression' &&
|
|
162
|
+
callee.property.type === 'Identifier'
|
|
163
|
+
)
|
|
164
|
+
calleeName = callee.property.name;
|
|
165
|
+
|
|
147
166
|
if (/telemetry|analytics|track|logEvent/i.test(calleeName)) {
|
|
148
167
|
return false; // Not "swallowed" in a bad way
|
|
149
168
|
}
|
|
@@ -164,7 +183,7 @@ export function detectDefensivePatterns(
|
|
|
164
183
|
const counts: PatternCounts = { ...ZERO_COUNTS };
|
|
165
184
|
const totalLines = code.split('\n').length;
|
|
166
185
|
|
|
167
|
-
let ast:
|
|
186
|
+
let ast: TSESTree.Program;
|
|
168
187
|
try {
|
|
169
188
|
ast = parse(code, {
|
|
170
189
|
filePath,
|
|
@@ -176,9 +195,9 @@ export function detectDefensivePatterns(
|
|
|
176
195
|
return { issues, counts, totalLines };
|
|
177
196
|
}
|
|
178
197
|
|
|
179
|
-
const nodesAtFunctionStart = new WeakSet<
|
|
198
|
+
const nodesAtFunctionStart = new WeakSet<TSESTree.Node>();
|
|
180
199
|
|
|
181
|
-
function markFunctionParamNodes(node:
|
|
200
|
+
function markFunctionParamNodes(node: TSESTree.Node) {
|
|
182
201
|
if (
|
|
183
202
|
node.type === 'FunctionDeclaration' ||
|
|
184
203
|
node.type === 'FunctionExpression' ||
|
|
@@ -191,7 +210,11 @@ export function detectDefensivePatterns(
|
|
|
191
210
|
}
|
|
192
211
|
}
|
|
193
212
|
|
|
194
|
-
function visit(
|
|
213
|
+
function visit(
|
|
214
|
+
node: TSESTree.Node | undefined,
|
|
215
|
+
_parent?: TSESTree.Node,
|
|
216
|
+
_keyInParent?: string
|
|
217
|
+
) {
|
|
195
218
|
if (!node || typeof node !== 'object') return;
|
|
196
219
|
|
|
197
220
|
markFunctionParamNodes(node);
|
|
@@ -213,9 +236,9 @@ export function detectDefensivePatterns(
|
|
|
213
236
|
Severity.Major,
|
|
214
237
|
'`as any` type assertion bypasses type safety',
|
|
215
238
|
filePath,
|
|
216
|
-
node.loc
|
|
217
|
-
node.loc
|
|
218
|
-
getLineContent(code, node.loc
|
|
239
|
+
node.loc.start.line,
|
|
240
|
+
node.loc.start.column,
|
|
241
|
+
getLineContent(code, node.loc.start.line)
|
|
219
242
|
)
|
|
220
243
|
);
|
|
221
244
|
}
|
|
@@ -238,9 +261,9 @@ export function detectDefensivePatterns(
|
|
|
238
261
|
Severity.Major,
|
|
239
262
|
'`as unknown` double-cast bypasses type safety',
|
|
240
263
|
filePath,
|
|
241
|
-
node.loc
|
|
242
|
-
node.loc
|
|
243
|
-
getLineContent(code, node.loc
|
|
264
|
+
node.loc.start.line,
|
|
265
|
+
node.loc.start.column,
|
|
266
|
+
getLineContent(code, node.loc.start.line)
|
|
244
267
|
)
|
|
245
268
|
);
|
|
246
269
|
}
|
|
@@ -257,9 +280,9 @@ export function detectDefensivePatterns(
|
|
|
257
280
|
Severity.Minor,
|
|
258
281
|
`Optional chain depth of ${depth} indicates missing structural guarantees`,
|
|
259
282
|
filePath,
|
|
260
|
-
node.loc
|
|
261
|
-
node.loc
|
|
262
|
-
getLineContent(code, node.loc
|
|
283
|
+
node.loc.start.line,
|
|
284
|
+
node.loc.start.column,
|
|
285
|
+
getLineContent(code, node.loc.start.line)
|
|
263
286
|
)
|
|
264
287
|
);
|
|
265
288
|
}
|
|
@@ -280,9 +303,9 @@ export function detectDefensivePatterns(
|
|
|
280
303
|
Severity.Minor,
|
|
281
304
|
'Nullish coalescing with literal default suggests missing upstream type guarantee',
|
|
282
305
|
filePath,
|
|
283
|
-
node.loc
|
|
284
|
-
node.loc
|
|
285
|
-
getLineContent(code, node.loc
|
|
306
|
+
node.loc.start.line,
|
|
307
|
+
node.loc.start.column,
|
|
308
|
+
getLineContent(code, node.loc.start.line)
|
|
286
309
|
)
|
|
287
310
|
);
|
|
288
311
|
}
|
|
@@ -299,9 +322,9 @@ export function detectDefensivePatterns(
|
|
|
299
322
|
Severity.Major,
|
|
300
323
|
'Error is swallowed in catch block — failures will be silent',
|
|
301
324
|
filePath,
|
|
302
|
-
node.handler.loc
|
|
303
|
-
node.handler.loc
|
|
304
|
-
getLineContent(code, node.handler.loc
|
|
325
|
+
node.handler.loc.start.line,
|
|
326
|
+
node.handler.loc.start.column,
|
|
327
|
+
getLineContent(code, node.handler.loc.start.line)
|
|
305
328
|
)
|
|
306
329
|
);
|
|
307
330
|
}
|
|
@@ -320,9 +343,9 @@ export function detectDefensivePatterns(
|
|
|
320
343
|
Severity.Minor,
|
|
321
344
|
'Environment variable with fallback — use a validated env schema instead',
|
|
322
345
|
filePath,
|
|
323
|
-
node.loc
|
|
324
|
-
node.loc
|
|
325
|
-
getLineContent(code, node.loc
|
|
346
|
+
node.loc.start.line,
|
|
347
|
+
node.loc.start.column,
|
|
348
|
+
getLineContent(code, node.loc.start.line)
|
|
326
349
|
)
|
|
327
350
|
);
|
|
328
351
|
}
|
|
@@ -352,9 +375,9 @@ export function detectDefensivePatterns(
|
|
|
352
375
|
Severity.Info,
|
|
353
376
|
'Guard clause could be eliminated with non-nullable type at source',
|
|
354
377
|
filePath,
|
|
355
|
-
node.loc
|
|
356
|
-
node.loc
|
|
357
|
-
getLineContent(code, node.loc
|
|
378
|
+
node.loc.start.line,
|
|
379
|
+
node.loc.start.column,
|
|
380
|
+
getLineContent(code, node.loc.start.line)
|
|
358
381
|
)
|
|
359
382
|
);
|
|
360
383
|
}
|
|
@@ -368,8 +391,14 @@ export function detectDefensivePatterns(
|
|
|
368
391
|
node.params
|
|
369
392
|
) {
|
|
370
393
|
for (const param of node.params) {
|
|
371
|
-
|
|
372
|
-
|
|
394
|
+
let typeAnno: TSESTree.TypeNode | undefined;
|
|
395
|
+
|
|
396
|
+
// Handle Identifier, AssignmentPattern, RestElement, etc.
|
|
397
|
+
if ('typeAnnotation' in param && param.typeAnnotation) {
|
|
398
|
+
typeAnno = (param.typeAnnotation as TSESTree.TSTypeAnnotation)
|
|
399
|
+
.typeAnnotation;
|
|
400
|
+
}
|
|
401
|
+
|
|
373
402
|
if (typeAnno?.type === 'TSAnyKeyword') {
|
|
374
403
|
counts['any-parameter']++;
|
|
375
404
|
issues.push(
|
|
@@ -378,40 +407,48 @@ export function detectDefensivePatterns(
|
|
|
378
407
|
Severity.Major,
|
|
379
408
|
'Parameter typed as `any` bypasses type safety',
|
|
380
409
|
filePath,
|
|
381
|
-
param.loc
|
|
382
|
-
param.loc
|
|
383
|
-
getLineContent(code, param.loc
|
|
410
|
+
param.loc.start.line,
|
|
411
|
+
param.loc.start.column,
|
|
412
|
+
getLineContent(code, param.loc.start.line)
|
|
384
413
|
)
|
|
385
414
|
);
|
|
386
415
|
}
|
|
387
416
|
}
|
|
388
417
|
|
|
389
418
|
// Pattern: any return type
|
|
390
|
-
|
|
419
|
+
let returnAnno: TSESTree.TypeNode | undefined;
|
|
420
|
+
if ('returnType' in node && node.returnType) {
|
|
421
|
+
returnAnno = (node.returnType as TSESTree.TSTypeAnnotation)
|
|
422
|
+
.typeAnnotation;
|
|
423
|
+
}
|
|
424
|
+
|
|
391
425
|
if (returnAnno?.type === 'TSAnyKeyword') {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
426
|
+
const fnNode = node as TSESTree.FunctionDeclaration;
|
|
427
|
+
if (fnNode.returnType?.loc) {
|
|
428
|
+
counts['any-return']++;
|
|
429
|
+
issues.push(
|
|
430
|
+
makeIssue(
|
|
431
|
+
'any-return',
|
|
432
|
+
Severity.Major,
|
|
433
|
+
'Return type is `any` — callers cannot rely on the result shape',
|
|
434
|
+
filePath,
|
|
435
|
+
fnNode.returnType.loc.start.line,
|
|
436
|
+
fnNode.returnType.loc.start.column,
|
|
437
|
+
getLineContent(code, fnNode.returnType.loc.start.line)
|
|
438
|
+
)
|
|
439
|
+
);
|
|
440
|
+
}
|
|
404
441
|
}
|
|
405
442
|
}
|
|
406
443
|
|
|
407
444
|
// Recurse
|
|
408
445
|
for (const key in node) {
|
|
409
446
|
if (key === 'loc' || key === 'range' || key === 'parent') continue;
|
|
410
|
-
const child = node[key];
|
|
447
|
+
const child = (node as Record<string, any>)[key];
|
|
411
448
|
if (Array.isArray(child)) {
|
|
412
449
|
for (const item of child) {
|
|
413
450
|
if (item && typeof item.type === 'string') {
|
|
414
|
-
visit(item, node, key);
|
|
451
|
+
visit(item as TSESTree.Node, node, key);
|
|
415
452
|
}
|
|
416
453
|
}
|
|
417
454
|
} else if (
|
|
@@ -419,7 +456,7 @@ export function detectDefensivePatterns(
|
|
|
419
456
|
typeof child === 'object' &&
|
|
420
457
|
typeof child.type === 'string'
|
|
421
458
|
) {
|
|
422
|
-
visit(child, node, key);
|
|
459
|
+
visit(child as TSESTree.Node, node, key);
|
|
423
460
|
}
|
|
424
461
|
}
|
|
425
462
|
}
|
package/src/provider.ts
CHANGED
|
@@ -30,11 +30,24 @@ export const ContractEnforcementProvider = createProvider({
|
|
|
30
30
|
},
|
|
31
31
|
|
|
32
32
|
score(output: SpokeOutput, _options: ScanOptions) {
|
|
33
|
-
const rawData =
|
|
34
|
-
|
|
33
|
+
const rawData = output.metadata?.rawData ?? {};
|
|
34
|
+
const result = calculateContractEnforcementScore(
|
|
35
35
|
rawData,
|
|
36
36
|
rawData.totalLines ?? 1,
|
|
37
37
|
rawData.sourceFiles ?? 1
|
|
38
|
-
)
|
|
38
|
+
);
|
|
39
|
+
return {
|
|
40
|
+
toolName: ToolName.ContractEnforcement,
|
|
41
|
+
score: result.score,
|
|
42
|
+
rating: result.rating,
|
|
43
|
+
recommendations: result.recommendations.map((action) => ({
|
|
44
|
+
action,
|
|
45
|
+
estimatedImpact: 10,
|
|
46
|
+
priority: 'medium' as const,
|
|
47
|
+
})),
|
|
48
|
+
dimensions: result.dimensions,
|
|
49
|
+
rawMetrics: rawData,
|
|
50
|
+
factors: [],
|
|
51
|
+
};
|
|
39
52
|
},
|
|
40
53
|
});
|