@aiready/contract-enforcement 0.2.1 → 0.2.2
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 +18 -19
- package/.turbo/turbo-test.log +5 -5
- package/dist/index.js +72 -42
- package/dist/index.mjs +72 -42
- package/package.json +2 -2
- package/src/detector.ts +94 -38
- package/src/scoring.ts +5 -4
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
DTS Build
|
|
17
|
-
DTS
|
|
18
|
-
DTS dist/index.d.
|
|
19
|
-
DTS dist/index.d.mts 2.45 KB
|
|
1
|
+
|
|
2
|
+
> @aiready/contract-enforcement@0.2.2 build /Users/pengcao/projects/aiready/packages/contract-enforcement
|
|
3
|
+
> tsup src/index.ts --format cjs,esm --dts
|
|
4
|
+
|
|
5
|
+
CLI Building entry: src/index.ts
|
|
6
|
+
CLI Using tsconfig: tsconfig.json
|
|
7
|
+
CLI tsup v8.5.1
|
|
8
|
+
CLI Target: es2020
|
|
9
|
+
CJS Build start
|
|
10
|
+
ESM Build start
|
|
11
|
+
ESM dist/index.mjs 16.58 KB
|
|
12
|
+
ESM ⚡️ Build success in 159ms
|
|
13
|
+
CJS dist/index.js 18.01 KB
|
|
14
|
+
CJS ⚡️ Build success in 159ms
|
|
15
|
+
DTS Build start
|
|
16
|
+
DTS ⚡️ Build success in 7234ms
|
|
17
|
+
DTS dist/index.d.ts 2.45 KB
|
|
18
|
+
DTS dist/index.d.mts 2.45 KB
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
|
|
2
|
-
> @aiready/contract-enforcement@0.1
|
|
2
|
+
> @aiready/contract-enforcement@0.2.1 test /Users/pengcao/projects/aiready/packages/contract-enforcement
|
|
3
3
|
> vitest run
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
[1m[46m RUN [49m[22m [36mv4.1.1 [39m[90m/Users/pengcao/projects/aiready/packages/contract-enforcement[39m
|
|
7
7
|
|
|
8
|
-
[32m✓[39m src/__tests__/scoring.test.ts [2m([22m[2m7 tests[22m[2m)[22m[32m
|
|
9
|
-
[32m✓[39m src/__tests__/detector.test.ts [2m([22m[2m14 tests[22m[2m)[22m[32m
|
|
8
|
+
[32m✓[39m src/__tests__/scoring.test.ts [2m([22m[2m7 tests[22m[2m)[22m[32m 62[2mms[22m[39m
|
|
9
|
+
[32m✓[39m src/__tests__/detector.test.ts [2m([22m[2m14 tests[22m[2m)[22m[32m 90[2mms[22m[39m
|
|
10
10
|
[32m✓[39m src/__tests__/provider.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 2[2mms[22m[39m
|
|
11
11
|
|
|
12
12
|
[2m Test Files [22m [1m[32m3 passed[39m[22m[90m (3)[39m
|
|
13
13
|
[2m Tests [22m [1m[32m26 passed[39m[22m[90m (26)[39m
|
|
14
|
-
[2m Start at [22m
|
|
15
|
-
[2m Duration [22m
|
|
14
|
+
[2m Start at [22m 14:03:18
|
|
15
|
+
[2m Duration [22m 3.32s[2m (transform 1.63s, setup 0ms, import 5.02s, tests 154ms, environment 0ms)[22m
|
|
16
16
|
|
package/dist/index.js
CHANGED
|
@@ -121,13 +121,37 @@ function isLiteral(node) {
|
|
|
121
121
|
function isProcessEnvAccess(node) {
|
|
122
122
|
return node?.type === "MemberExpression" && node.object?.type === "MemberExpression" && node.object.object?.name === "process" && node.object.property?.name === "env";
|
|
123
123
|
}
|
|
124
|
-
function
|
|
124
|
+
function isSstResourceAccess(node) {
|
|
125
|
+
if (!node) return false;
|
|
126
|
+
let current = node;
|
|
127
|
+
if (current.type === "ChainExpression") {
|
|
128
|
+
current = current.expression;
|
|
129
|
+
}
|
|
130
|
+
while (current && current.type === "MemberExpression") {
|
|
131
|
+
if (current.object?.type === "Identifier" && current.object.name === "Resource") {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
current = current.object;
|
|
135
|
+
}
|
|
136
|
+
if (current?.type === "Identifier" && current.name === "Resource") {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
function isSwallowedCatch(body, filePath) {
|
|
125
142
|
if (body.length === 0) return true;
|
|
143
|
+
const isUiComponent = filePath.endsWith(".tsx") || filePath.endsWith(".jsx");
|
|
126
144
|
if (body.length === 1) {
|
|
127
145
|
const stmt = body[0];
|
|
128
146
|
if (stmt.type === "ExpressionStatement" && stmt.expression?.type === "CallExpression") {
|
|
129
147
|
const callee = stmt.expression.callee;
|
|
130
148
|
if (callee?.object?.name === "console") return true;
|
|
149
|
+
if (isUiComponent) {
|
|
150
|
+
const calleeName = callee?.name || callee?.property?.name || "";
|
|
151
|
+
if (/telemetry|analytics|track|logEvent/i.test(calleeName)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
131
155
|
}
|
|
132
156
|
if (stmt.type === "ThrowStatement") return false;
|
|
133
157
|
}
|
|
@@ -161,32 +185,36 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
161
185
|
if (!node || typeof node !== "object") return;
|
|
162
186
|
markFunctionParamNodes(node);
|
|
163
187
|
if (node.type === "TSAsExpression" && node.typeAnnotation?.type === "TSAnyKeyword") {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
188
|
+
if (!isSstResourceAccess(node.expression) && !isProcessEnvAccess(node.expression)) {
|
|
189
|
+
counts["as-any"]++;
|
|
190
|
+
issues.push(
|
|
191
|
+
makeIssue(
|
|
192
|
+
"as-any",
|
|
193
|
+
import_core.Severity.Major,
|
|
194
|
+
"`as any` type assertion bypasses type safety",
|
|
195
|
+
filePath,
|
|
196
|
+
node.loc?.start.line ?? 0,
|
|
197
|
+
node.loc?.start.column ?? 0,
|
|
198
|
+
getLineContent(code, node.loc?.start.line ?? 0)
|
|
199
|
+
)
|
|
200
|
+
);
|
|
201
|
+
}
|
|
176
202
|
}
|
|
177
203
|
if (node.type === "TSAsExpression" && node.typeAnnotation?.type === "TSUnknownKeyword") {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
204
|
+
if (!isSstResourceAccess(node.expression) && !isProcessEnvAccess(node.expression)) {
|
|
205
|
+
counts["as-unknown"]++;
|
|
206
|
+
issues.push(
|
|
207
|
+
makeIssue(
|
|
208
|
+
"as-unknown",
|
|
209
|
+
import_core.Severity.Major,
|
|
210
|
+
"`as unknown` double-cast bypasses type safety",
|
|
211
|
+
filePath,
|
|
212
|
+
node.loc?.start.line ?? 0,
|
|
213
|
+
node.loc?.start.column ?? 0,
|
|
214
|
+
getLineContent(code, node.loc?.start.line ?? 0)
|
|
215
|
+
)
|
|
216
|
+
);
|
|
217
|
+
}
|
|
190
218
|
}
|
|
191
219
|
if (node.type === "ChainExpression") {
|
|
192
220
|
const depth = countOptionalChainDepth(node);
|
|
@@ -206,22 +234,24 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
206
234
|
}
|
|
207
235
|
}
|
|
208
236
|
if (node.type === "LogicalExpression" && node.operator === "??" && isLiteral(node.right)) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
237
|
+
if (!isSstResourceAccess(node.left) && !isProcessEnvAccess(node.left)) {
|
|
238
|
+
counts["nullish-literal-default"]++;
|
|
239
|
+
issues.push(
|
|
240
|
+
makeIssue(
|
|
241
|
+
"nullish-literal-default",
|
|
242
|
+
import_core.Severity.Minor,
|
|
243
|
+
"Nullish coalescing with literal default suggests missing upstream type guarantee",
|
|
244
|
+
filePath,
|
|
245
|
+
node.loc?.start.line ?? 0,
|
|
246
|
+
node.loc?.start.column ?? 0,
|
|
247
|
+
getLineContent(code, node.loc?.start.line ?? 0)
|
|
248
|
+
)
|
|
249
|
+
);
|
|
250
|
+
}
|
|
221
251
|
}
|
|
222
252
|
if (node.type === "TryStatement" && node.handler) {
|
|
223
253
|
const catchBody = node.handler.body?.body;
|
|
224
|
-
if (catchBody && isSwallowedCatch(catchBody)) {
|
|
254
|
+
if (catchBody && isSwallowedCatch(catchBody, filePath)) {
|
|
225
255
|
counts["swallowed-error"]++;
|
|
226
256
|
issues.push(
|
|
227
257
|
makeIssue(
|
|
@@ -345,10 +375,10 @@ function calculateContractEnforcementScore(counts, totalLines, _fileCount) {
|
|
|
345
375
|
const fallbackDensity = fallbackCount / loc * 1e3;
|
|
346
376
|
const errorDensity = errorCount / loc * 1e3;
|
|
347
377
|
const boundaryDensity = boundaryCount / loc * 1e3;
|
|
348
|
-
const typeEscapeHatchScore = clamp(100 - typeDensity *
|
|
349
|
-
const fallbackCascadeScore = clamp(100 - fallbackDensity *
|
|
350
|
-
const errorTransparencyScore = clamp(100 - errorDensity *
|
|
351
|
-
const boundaryValidationScore = clamp(100 - boundaryDensity *
|
|
378
|
+
const typeEscapeHatchScore = clamp(100 - typeDensity * 10, 0, 100);
|
|
379
|
+
const fallbackCascadeScore = clamp(100 - fallbackDensity * 8, 0, 100);
|
|
380
|
+
const errorTransparencyScore = clamp(100 - errorDensity * 15, 0, 100);
|
|
381
|
+
const boundaryValidationScore = clamp(100 - boundaryDensity * 7, 0, 100);
|
|
352
382
|
const score = Math.round(
|
|
353
383
|
typeEscapeHatchScore * DIMENSION_WEIGHTS.typeEscapeHatch + fallbackCascadeScore * DIMENSION_WEIGHTS.fallbackCascade + errorTransparencyScore * DIMENSION_WEIGHTS.errorTransparency + boundaryValidationScore * DIMENSION_WEIGHTS.boundaryValidation
|
|
354
384
|
);
|
package/dist/index.mjs
CHANGED
|
@@ -94,13 +94,37 @@ function isLiteral(node) {
|
|
|
94
94
|
function isProcessEnvAccess(node) {
|
|
95
95
|
return node?.type === "MemberExpression" && node.object?.type === "MemberExpression" && node.object.object?.name === "process" && node.object.property?.name === "env";
|
|
96
96
|
}
|
|
97
|
-
function
|
|
97
|
+
function isSstResourceAccess(node) {
|
|
98
|
+
if (!node) return false;
|
|
99
|
+
let current = node;
|
|
100
|
+
if (current.type === "ChainExpression") {
|
|
101
|
+
current = current.expression;
|
|
102
|
+
}
|
|
103
|
+
while (current && current.type === "MemberExpression") {
|
|
104
|
+
if (current.object?.type === "Identifier" && current.object.name === "Resource") {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
current = current.object;
|
|
108
|
+
}
|
|
109
|
+
if (current?.type === "Identifier" && current.name === "Resource") {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
function isSwallowedCatch(body, filePath) {
|
|
98
115
|
if (body.length === 0) return true;
|
|
116
|
+
const isUiComponent = filePath.endsWith(".tsx") || filePath.endsWith(".jsx");
|
|
99
117
|
if (body.length === 1) {
|
|
100
118
|
const stmt = body[0];
|
|
101
119
|
if (stmt.type === "ExpressionStatement" && stmt.expression?.type === "CallExpression") {
|
|
102
120
|
const callee = stmt.expression.callee;
|
|
103
121
|
if (callee?.object?.name === "console") return true;
|
|
122
|
+
if (isUiComponent) {
|
|
123
|
+
const calleeName = callee?.name || callee?.property?.name || "";
|
|
124
|
+
if (/telemetry|analytics|track|logEvent/i.test(calleeName)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
104
128
|
}
|
|
105
129
|
if (stmt.type === "ThrowStatement") return false;
|
|
106
130
|
}
|
|
@@ -134,32 +158,36 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
134
158
|
if (!node || typeof node !== "object") return;
|
|
135
159
|
markFunctionParamNodes(node);
|
|
136
160
|
if (node.type === "TSAsExpression" && node.typeAnnotation?.type === "TSAnyKeyword") {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
161
|
+
if (!isSstResourceAccess(node.expression) && !isProcessEnvAccess(node.expression)) {
|
|
162
|
+
counts["as-any"]++;
|
|
163
|
+
issues.push(
|
|
164
|
+
makeIssue(
|
|
165
|
+
"as-any",
|
|
166
|
+
Severity.Major,
|
|
167
|
+
"`as any` type assertion bypasses type safety",
|
|
168
|
+
filePath,
|
|
169
|
+
node.loc?.start.line ?? 0,
|
|
170
|
+
node.loc?.start.column ?? 0,
|
|
171
|
+
getLineContent(code, node.loc?.start.line ?? 0)
|
|
172
|
+
)
|
|
173
|
+
);
|
|
174
|
+
}
|
|
149
175
|
}
|
|
150
176
|
if (node.type === "TSAsExpression" && node.typeAnnotation?.type === "TSUnknownKeyword") {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
177
|
+
if (!isSstResourceAccess(node.expression) && !isProcessEnvAccess(node.expression)) {
|
|
178
|
+
counts["as-unknown"]++;
|
|
179
|
+
issues.push(
|
|
180
|
+
makeIssue(
|
|
181
|
+
"as-unknown",
|
|
182
|
+
Severity.Major,
|
|
183
|
+
"`as unknown` double-cast bypasses type safety",
|
|
184
|
+
filePath,
|
|
185
|
+
node.loc?.start.line ?? 0,
|
|
186
|
+
node.loc?.start.column ?? 0,
|
|
187
|
+
getLineContent(code, node.loc?.start.line ?? 0)
|
|
188
|
+
)
|
|
189
|
+
);
|
|
190
|
+
}
|
|
163
191
|
}
|
|
164
192
|
if (node.type === "ChainExpression") {
|
|
165
193
|
const depth = countOptionalChainDepth(node);
|
|
@@ -179,22 +207,24 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
179
207
|
}
|
|
180
208
|
}
|
|
181
209
|
if (node.type === "LogicalExpression" && node.operator === "??" && isLiteral(node.right)) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
210
|
+
if (!isSstResourceAccess(node.left) && !isProcessEnvAccess(node.left)) {
|
|
211
|
+
counts["nullish-literal-default"]++;
|
|
212
|
+
issues.push(
|
|
213
|
+
makeIssue(
|
|
214
|
+
"nullish-literal-default",
|
|
215
|
+
Severity.Minor,
|
|
216
|
+
"Nullish coalescing with literal default suggests missing upstream type guarantee",
|
|
217
|
+
filePath,
|
|
218
|
+
node.loc?.start.line ?? 0,
|
|
219
|
+
node.loc?.start.column ?? 0,
|
|
220
|
+
getLineContent(code, node.loc?.start.line ?? 0)
|
|
221
|
+
)
|
|
222
|
+
);
|
|
223
|
+
}
|
|
194
224
|
}
|
|
195
225
|
if (node.type === "TryStatement" && node.handler) {
|
|
196
226
|
const catchBody = node.handler.body?.body;
|
|
197
|
-
if (catchBody && isSwallowedCatch(catchBody)) {
|
|
227
|
+
if (catchBody && isSwallowedCatch(catchBody, filePath)) {
|
|
198
228
|
counts["swallowed-error"]++;
|
|
199
229
|
issues.push(
|
|
200
230
|
makeIssue(
|
|
@@ -318,10 +348,10 @@ function calculateContractEnforcementScore(counts, totalLines, _fileCount) {
|
|
|
318
348
|
const fallbackDensity = fallbackCount / loc * 1e3;
|
|
319
349
|
const errorDensity = errorCount / loc * 1e3;
|
|
320
350
|
const boundaryDensity = boundaryCount / loc * 1e3;
|
|
321
|
-
const typeEscapeHatchScore = clamp(100 - typeDensity *
|
|
322
|
-
const fallbackCascadeScore = clamp(100 - fallbackDensity *
|
|
323
|
-
const errorTransparencyScore = clamp(100 - errorDensity *
|
|
324
|
-
const boundaryValidationScore = clamp(100 - boundaryDensity *
|
|
351
|
+
const typeEscapeHatchScore = clamp(100 - typeDensity * 10, 0, 100);
|
|
352
|
+
const fallbackCascadeScore = clamp(100 - fallbackDensity * 8, 0, 100);
|
|
353
|
+
const errorTransparencyScore = clamp(100 - errorDensity * 15, 0, 100);
|
|
354
|
+
const boundaryValidationScore = clamp(100 - boundaryDensity * 7, 0, 100);
|
|
325
355
|
const score = Math.round(
|
|
326
356
|
typeEscapeHatchScore * DIMENSION_WEIGHTS.typeEscapeHatch + fallbackCascadeScore * DIMENSION_WEIGHTS.fallbackCascade + errorTransparencyScore * DIMENSION_WEIGHTS.errorTransparency + boundaryValidationScore * DIMENSION_WEIGHTS.boundaryValidation
|
|
327
357
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/contract-enforcement",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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.2"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^24.0.0",
|
package/src/detector.ts
CHANGED
|
@@ -98,8 +98,39 @@ function isProcessEnvAccess(node: any): boolean {
|
|
|
98
98
|
);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
function
|
|
101
|
+
function isSstResourceAccess(node: any): boolean {
|
|
102
|
+
if (!node) return false;
|
|
103
|
+
let current = node;
|
|
104
|
+
// Handle ChainExpression for optional chaining like Resource.MySecret?.value
|
|
105
|
+
if (current.type === 'ChainExpression') {
|
|
106
|
+
current = current.expression;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Recursively check for 'Resource' identifier at the base of the member chain
|
|
110
|
+
while (current && current.type === 'MemberExpression') {
|
|
111
|
+
if (
|
|
112
|
+
current.object?.type === 'Identifier' &&
|
|
113
|
+
current.object.name === 'Resource'
|
|
114
|
+
) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
current = current.object;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Also check if the node itself is 'Resource' (rare but possible in some contexts)
|
|
121
|
+
if (current?.type === 'Identifier' && current.name === 'Resource') {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isSwallowedCatch(body: any[], filePath: string): boolean {
|
|
102
129
|
if (body.length === 0) return true;
|
|
130
|
+
|
|
131
|
+
// UI components often have intentional silent catches for telemetry/analytics
|
|
132
|
+
const isUiComponent = filePath.endsWith('.tsx') || filePath.endsWith('.jsx');
|
|
133
|
+
|
|
103
134
|
if (body.length === 1) {
|
|
104
135
|
const stmt = body[0];
|
|
105
136
|
if (
|
|
@@ -107,10 +138,20 @@ function isSwallowedCatch(body: any[]): boolean {
|
|
|
107
138
|
stmt.expression?.type === 'CallExpression'
|
|
108
139
|
) {
|
|
109
140
|
const callee = stmt.expression.callee;
|
|
141
|
+
// console.log/warn/error is still considered "swallowed" but might be acceptable
|
|
110
142
|
if (callee?.object?.name === 'console') return true;
|
|
143
|
+
|
|
144
|
+
// If it's a UI component and looks like telemetry, it's a false positive
|
|
145
|
+
if (isUiComponent) {
|
|
146
|
+
const calleeName = callee?.name || callee?.property?.name || '';
|
|
147
|
+
if (/telemetry|analytics|track|logEvent/i.test(calleeName)) {
|
|
148
|
+
return false; // Not "swallowed" in a bad way
|
|
149
|
+
}
|
|
150
|
+
}
|
|
111
151
|
}
|
|
112
152
|
if (stmt.type === 'ThrowStatement') return false;
|
|
113
153
|
}
|
|
154
|
+
|
|
114
155
|
return false;
|
|
115
156
|
}
|
|
116
157
|
|
|
@@ -160,18 +201,24 @@ export function detectDefensivePatterns(
|
|
|
160
201
|
node.type === 'TSAsExpression' &&
|
|
161
202
|
node.typeAnnotation?.type === 'TSAnyKeyword'
|
|
162
203
|
) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
204
|
+
// Ignore if it's acting on an SST resource or process.env, common "necessary escape hatches"
|
|
205
|
+
if (
|
|
206
|
+
!isSstResourceAccess(node.expression) &&
|
|
207
|
+
!isProcessEnvAccess(node.expression)
|
|
208
|
+
) {
|
|
209
|
+
counts['as-any']++;
|
|
210
|
+
issues.push(
|
|
211
|
+
makeIssue(
|
|
212
|
+
'as-any',
|
|
213
|
+
Severity.Major,
|
|
214
|
+
'`as any` type assertion bypasses type safety',
|
|
215
|
+
filePath,
|
|
216
|
+
node.loc?.start.line ?? 0,
|
|
217
|
+
node.loc?.start.column ?? 0,
|
|
218
|
+
getLineContent(code, node.loc?.start.line ?? 0)
|
|
219
|
+
)
|
|
220
|
+
);
|
|
221
|
+
}
|
|
175
222
|
}
|
|
176
223
|
|
|
177
224
|
// Pattern: as unknown
|
|
@@ -179,18 +226,24 @@ export function detectDefensivePatterns(
|
|
|
179
226
|
node.type === 'TSAsExpression' &&
|
|
180
227
|
node.typeAnnotation?.type === 'TSUnknownKeyword'
|
|
181
228
|
) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
229
|
+
// Ignore if it's acting on an SST resource or process.env
|
|
230
|
+
if (
|
|
231
|
+
!isSstResourceAccess(node.expression) &&
|
|
232
|
+
!isProcessEnvAccess(node.expression)
|
|
233
|
+
) {
|
|
234
|
+
counts['as-unknown']++;
|
|
235
|
+
issues.push(
|
|
236
|
+
makeIssue(
|
|
237
|
+
'as-unknown',
|
|
238
|
+
Severity.Major,
|
|
239
|
+
'`as unknown` double-cast bypasses type safety',
|
|
240
|
+
filePath,
|
|
241
|
+
node.loc?.start.line ?? 0,
|
|
242
|
+
node.loc?.start.column ?? 0,
|
|
243
|
+
getLineContent(code, node.loc?.start.line ?? 0)
|
|
244
|
+
)
|
|
245
|
+
);
|
|
246
|
+
}
|
|
194
247
|
}
|
|
195
248
|
|
|
196
249
|
// Pattern: deep optional chaining
|
|
@@ -218,24 +271,27 @@ export function detectDefensivePatterns(
|
|
|
218
271
|
node.operator === '??' &&
|
|
219
272
|
isLiteral(node.right)
|
|
220
273
|
) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
274
|
+
// Ignore if it's an SST Resource or process.env - the standard way to handle fallbacks
|
|
275
|
+
if (!isSstResourceAccess(node.left) && !isProcessEnvAccess(node.left)) {
|
|
276
|
+
counts['nullish-literal-default']++;
|
|
277
|
+
issues.push(
|
|
278
|
+
makeIssue(
|
|
279
|
+
'nullish-literal-default',
|
|
280
|
+
Severity.Minor,
|
|
281
|
+
'Nullish coalescing with literal default suggests missing upstream type guarantee',
|
|
282
|
+
filePath,
|
|
283
|
+
node.loc?.start.line ?? 0,
|
|
284
|
+
node.loc?.start.column ?? 0,
|
|
285
|
+
getLineContent(code, node.loc?.start.line ?? 0)
|
|
286
|
+
)
|
|
287
|
+
);
|
|
288
|
+
}
|
|
233
289
|
}
|
|
234
290
|
|
|
235
291
|
// Pattern: swallowed error
|
|
236
292
|
if (node.type === 'TryStatement' && node.handler) {
|
|
237
293
|
const catchBody = node.handler.body?.body;
|
|
238
|
-
if (catchBody && isSwallowedCatch(catchBody)) {
|
|
294
|
+
if (catchBody && isSwallowedCatch(catchBody, filePath)) {
|
|
239
295
|
counts['swallowed-error']++;
|
|
240
296
|
issues.push(
|
|
241
297
|
makeIssue(
|
package/src/scoring.ts
CHANGED
|
@@ -35,10 +35,11 @@ export function calculateContractEnforcementScore(
|
|
|
35
35
|
const boundaryDensity = (boundaryCount / loc) * 1000;
|
|
36
36
|
|
|
37
37
|
// Dimension scores: 100 = no patterns, 0 = very high density
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
38
|
+
// Adjusted to be more lenient - many defensive patterns are valid practices
|
|
39
|
+
const typeEscapeHatchScore = clamp(100 - typeDensity * 10, 0, 100);
|
|
40
|
+
const fallbackCascadeScore = clamp(100 - fallbackDensity * 8, 0, 100);
|
|
41
|
+
const errorTransparencyScore = clamp(100 - errorDensity * 15, 0, 100);
|
|
42
|
+
const boundaryValidationScore = clamp(100 - boundaryDensity * 7, 0, 100);
|
|
42
43
|
|
|
43
44
|
const score = Math.round(
|
|
44
45
|
typeEscapeHatchScore * DIMENSION_WEIGHTS.typeEscapeHatch +
|