@skyramp/mcp 0.3.6 → 0.3.8
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/build/prompts/code-reuse.js +17 -2
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +1 -1
- package/build/prompts/modularization/ui-test-modularization.js +9 -6
- package/build/prompts/pom-aware-code-reuse.js +1 -1
- package/build/prompts/shared-helper-policy.js +5 -5
- package/build/prompts/test-recommendation/diffExecutionPlan.js +40 -9
- package/build/prompts/test-recommendation/fullRepoCatalog.js +3 -2
- package/build/prompts/test-recommendation/recommendationSections.js +3 -3
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +2 -2
- package/build/prompts/test-recommendation/scopeAssessment.js +58 -76
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +14 -1
- package/build/prompts/testbot/testbot-prompts.js +25 -8
- package/build/recommendation/budgeters/diversityBalancedBudgeter.js +39 -5
- package/build/recommendation/budgeters/shared.d.ts +21 -8
- package/build/recommendation/budgeters/shared.js +134 -56
- package/build/recommendation/planRanker.d.ts +5 -3
- package/build/recommendation/planRanker.js +1 -1
- package/build/services/TestGenerationService.js +15 -1
- package/build/tools/code-refactor/assertion-state.d.ts +91 -0
- package/build/tools/code-refactor/assertion-state.js +375 -0
- package/build/tools/code-refactor/codeReuseTool.js +6 -4
- package/build/tools/code-refactor/enhanceAssertionsTool.js +73 -18
- package/build/tools/code-refactor/retrofit-state.d.ts +53 -0
- package/build/tools/code-refactor/retrofit-state.js +162 -0
- package/build/tools/code-refactor/reuse-outcome.d.ts +7 -0
- package/build/tools/code-refactor/reuse-state.d.ts +9 -0
- package/build/tools/code-refactor/reuse-state.js +42 -4
- package/build/tools/code-refactor/utils-verify-gates.js +69 -15
- package/build/tools/executeSkyrampTestTool.js +19 -14
- package/build/tools/submitReportTool.js +87 -9
- package/build/tools/test-management/registerTestPlanTool.d.ts +3 -3
- package/build/tools/test-management/registerTestPlanTool.js +29 -8
- package/build/types/AssertionOutcome.d.ts +68 -0
- package/build/types/AssertionOutcome.js +1 -0
- package/build/types/RepositoryAnalysis.d.ts +10 -10
- package/build/types/ReuseOutcome.d.ts +16 -0
- package/build/types/TestRecommendation.d.ts +11 -1
- package/build/types/TestRecommendation.js +34 -0
- package/build/types/TestTypes.d.ts +4 -0
- package/build/types/TestTypes.js +8 -0
- package/build/types/TestbotReport.d.ts +13 -0
- package/build/types/index.d.ts +1 -1
- package/build/utils/AnalysisStateManager.d.ts +20 -7
- package/build/utils/assertion-verify/api-shared-lints.d.ts +5 -0
- package/build/utils/assertion-verify/api-shared-lints.js +315 -0
- package/build/utils/assertion-verify/contract-lints.d.ts +3 -0
- package/build/utils/assertion-verify/contract-lints.js +87 -0
- package/build/utils/assertion-verify/format.d.ts +5 -0
- package/build/utils/assertion-verify/format.js +65 -0
- package/build/utils/assertion-verify/helper-imports.d.ts +6 -0
- package/build/utils/assertion-verify/helper-imports.js +178 -0
- package/build/utils/assertion-verify/index.d.ts +3 -0
- package/build/utils/assertion-verify/index.js +7 -0
- package/build/utils/assertion-verify/integration-lints.d.ts +3 -0
- package/build/utils/assertion-verify/integration-lints.js +36 -0
- package/build/utils/assertion-verify/js-regex-blank.d.ts +1 -0
- package/build/utils/assertion-verify/js-regex-blank.js +153 -0
- package/build/utils/assertion-verify/lint-types.d.ts +33 -0
- package/build/utils/assertion-verify/lint-types.js +57 -0
- package/build/utils/assertion-verify/marker.d.ts +27 -0
- package/build/utils/assertion-verify/marker.js +61 -0
- package/build/utils/assertion-verify/metrics.d.ts +30 -0
- package/build/utils/assertion-verify/metrics.js +341 -0
- package/build/utils/assertion-verify/python-strip.d.ts +6 -0
- package/build/utils/assertion-verify/python-strip.js +75 -0
- package/build/utils/assertion-verify/strip-dispatch.d.ts +19 -0
- package/build/utils/assertion-verify/strip-dispatch.js +42 -0
- package/build/utils/assertion-verify/ui-lints.d.ts +8 -0
- package/build/utils/assertion-verify/ui-lints.js +244 -0
- package/build/utils/assertion-verify/verify.d.ts +61 -0
- package/build/utils/assertion-verify/verify.js +215 -0
- package/build/utils/featureFlags.d.ts +12 -2
- package/build/utils/featureFlags.js +33 -3
- package/build/utils/pathSignatures.d.ts +4 -1
- package/build/utils/pathSignatures.js +14 -2
- package/build/utils/planOnlyMode.d.ts +33 -0
- package/build/utils/planOnlyMode.js +40 -0
- package/build/utils/reportVerification.d.ts +9 -0
- package/build/utils/reportVerification.js +39 -6
- package/build/utils/scenarioDrafting.d.ts +1 -1
- package/build/utils/scenarioDrafting.js +1 -1
- package/build/utils/utils-verify/allow.d.ts +22 -4
- package/build/utils/utils-verify/allow.js +8 -2
- package/build/utils/utils-verify/call-sites.d.ts +40 -1
- package/build/utils/utils-verify/call-sites.js +196 -30
- package/build/utils/utils-verify/importers.d.ts +31 -0
- package/build/utils/utils-verify/importers.js +78 -0
- package/build/utils/utils-verify/index.d.ts +1 -0
- package/build/utils/utils-verify/index.js +1 -0
- package/build/utils/utils-verify/language-spec.d.ts +13 -2
- package/build/utils/utils-verify/language-spec.js +12 -2
- package/build/utils/utils-verify/parse.d.ts +31 -3
- package/build/utils/utils-verify/parse.js +190 -9
- package/build/utils/utils-verify/retrofit-equivalence.d.ts +43 -0
- package/build/utils/utils-verify/retrofit-equivalence.js +218 -0
- package/build/utils/utils-verify/stage.d.ts +6 -0
- package/build/utils/utils-verify/stage.js +12 -2
- package/build/utils/utils-verify/verify.d.ts +54 -4
- package/build/utils/utils-verify/verify.js +224 -12
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-CZfp96qZ.js → codeMirrorModule-LNgEKtdV.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-gpLo02E0.js → defaultSettingsView-Bwr1eMKC.js} +135 -135
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Bq1r1URj.js → index.-Id052Lr.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.VEfqi1qN.js → uiMode.BPopbasy.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +2 -2
- package/build/tools/code-refactor/enhance-state.d.ts +0 -49
- package/build/tools/code-refactor/enhance-state.js +0 -109
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { strippedSources } from "./strip-dispatch.js";
|
|
3
|
+
import { balancedCloseIndex, lineOfOffset } from "./lint-types.js";
|
|
4
|
+
const FINGERPRINT_CHAR_CAP = 120;
|
|
5
|
+
const FINGERPRINT_SITE_CAP = 500;
|
|
6
|
+
/** Exact-value matchers (camelCase and playwright-python snake_case twins). */
|
|
7
|
+
const EXACT_MATCHERS = new Set([
|
|
8
|
+
"toHaveText",
|
|
9
|
+
"toHaveValue",
|
|
10
|
+
"toHaveAttribute",
|
|
11
|
+
"toHaveCount",
|
|
12
|
+
"toBeChecked",
|
|
13
|
+
"toHaveLength",
|
|
14
|
+
"toBe",
|
|
15
|
+
"toEqual",
|
|
16
|
+
"toStrictEqual",
|
|
17
|
+
"toBeUndefined",
|
|
18
|
+
"to_have_text",
|
|
19
|
+
"to_have_value",
|
|
20
|
+
"to_have_attribute",
|
|
21
|
+
"to_have_count",
|
|
22
|
+
"to_be_checked",
|
|
23
|
+
"to_have_length",
|
|
24
|
+
"to_be",
|
|
25
|
+
"to_equal",
|
|
26
|
+
"toHaveJSProperty",
|
|
27
|
+
"to_have_js_property",
|
|
28
|
+
]);
|
|
29
|
+
/** Pattern/partial/state matchers — weaker than exact values, but real
|
|
30
|
+
* behavioral assertions: `toBeDisabled` after an action asserts a semantic
|
|
31
|
+
* state transition, not mere presence, so it must clear the weak-additions
|
|
32
|
+
* gate (weight ≥ 2). */
|
|
33
|
+
const PARTIAL_MATCHERS = new Set([
|
|
34
|
+
"toMatch",
|
|
35
|
+
"toContainText",
|
|
36
|
+
"toHaveClass",
|
|
37
|
+
"toHaveURL",
|
|
38
|
+
"toContainEqual",
|
|
39
|
+
"toBeDisabled",
|
|
40
|
+
"toBeEnabled",
|
|
41
|
+
"toBeHidden",
|
|
42
|
+
"to_match",
|
|
43
|
+
"to_contain_text",
|
|
44
|
+
"to_have_class",
|
|
45
|
+
"to_have_url",
|
|
46
|
+
"to_be_disabled",
|
|
47
|
+
"to_be_enabled",
|
|
48
|
+
"to_be_hidden",
|
|
49
|
+
]);
|
|
50
|
+
/** Chain links to skip between `expect(...)` and the matcher. */
|
|
51
|
+
const CHAIN_LINKS = new Set([
|
|
52
|
+
"not",
|
|
53
|
+
"resolves",
|
|
54
|
+
"rejects",
|
|
55
|
+
"soft",
|
|
56
|
+
"not_to",
|
|
57
|
+
"to",
|
|
58
|
+
]);
|
|
59
|
+
export function detectAssertionLanguage(testFile) {
|
|
60
|
+
switch (path.extname(testFile).toLowerCase()) {
|
|
61
|
+
case ".py":
|
|
62
|
+
return "python";
|
|
63
|
+
case ".ts":
|
|
64
|
+
case ".tsx":
|
|
65
|
+
case ".mts":
|
|
66
|
+
return "typescript";
|
|
67
|
+
case ".js":
|
|
68
|
+
case ".jsx":
|
|
69
|
+
case ".mjs":
|
|
70
|
+
case ".cjs":
|
|
71
|
+
return "javascript";
|
|
72
|
+
case ".java":
|
|
73
|
+
return "java";
|
|
74
|
+
default:
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function matcherWeight(matcher) {
|
|
79
|
+
if (matcher === undefined)
|
|
80
|
+
return 1;
|
|
81
|
+
if (EXACT_MATCHERS.has(matcher))
|
|
82
|
+
return 3;
|
|
83
|
+
if (PARTIAL_MATCHERS.has(matcher))
|
|
84
|
+
return 2;
|
|
85
|
+
// Unknown matchers weigh 1, never 0 — any added assertion raises the score.
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
function fingerprintOf(raw, start, end) {
|
|
89
|
+
return raw
|
|
90
|
+
.slice(start, end)
|
|
91
|
+
.replace(/\s+/g, " ")
|
|
92
|
+
.trim()
|
|
93
|
+
.slice(0, FINGERPRINT_CHAR_CAP);
|
|
94
|
+
}
|
|
95
|
+
function startOfLine(src, offset) {
|
|
96
|
+
const nl = src.lastIndexOf("\n", offset - 1);
|
|
97
|
+
return nl === -1 ? 0 : nl + 1;
|
|
98
|
+
}
|
|
99
|
+
/** expect-chain sites (`expect(...)[.not/.resolves/...].matcher(...)`), shared by
|
|
100
|
+
* TS/JS and playwright-python. Scans stripped content, fingerprints from raw. */
|
|
101
|
+
function expectSites(raw, stripped) {
|
|
102
|
+
const sites = [];
|
|
103
|
+
const re = /\bexpect(?:\s*\.\s*soft)?\s*\(/g;
|
|
104
|
+
let m;
|
|
105
|
+
while ((m = re.exec(stripped)) !== null) {
|
|
106
|
+
const openIdx = m.index + m[0].length - 1;
|
|
107
|
+
const argClose = balancedCloseIndex(stripped, openIdx);
|
|
108
|
+
if (argClose === -1)
|
|
109
|
+
continue;
|
|
110
|
+
// Walk the chain past link members (.not / .resolves / …) to the matcher.
|
|
111
|
+
let cursor = argClose + 1;
|
|
112
|
+
let matcher;
|
|
113
|
+
for (;;) {
|
|
114
|
+
const link = /^\s*\.\s*([A-Za-z_]\w*)/.exec(stripped.slice(cursor));
|
|
115
|
+
if (!link)
|
|
116
|
+
break;
|
|
117
|
+
cursor += link[0].length;
|
|
118
|
+
if (CHAIN_LINKS.has(link[1]))
|
|
119
|
+
continue;
|
|
120
|
+
matcher = link[1];
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
// Statement extent: start of the site's line through the matcher call's
|
|
124
|
+
// closing paren (or the expect close when there is no chained call).
|
|
125
|
+
let stmtEnd = argClose + 1;
|
|
126
|
+
if (matcher !== undefined) {
|
|
127
|
+
const matcherOpen = /^\s*\(/.exec(stripped.slice(cursor));
|
|
128
|
+
if (matcherOpen) {
|
|
129
|
+
const close = balancedCloseIndex(stripped, cursor + matcherOpen[0].length - 1);
|
|
130
|
+
if (close !== -1)
|
|
131
|
+
stmtEnd = close + 1;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
stmtEnd = cursor;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const stmtStart = startOfLine(raw, m.index);
|
|
138
|
+
sites.push({
|
|
139
|
+
line: lineOfOffset(stripped, m.index),
|
|
140
|
+
matcher,
|
|
141
|
+
weight: matcherWeight(matcher),
|
|
142
|
+
fingerprint: fingerprintOf(raw, stmtStart, stmtEnd),
|
|
143
|
+
});
|
|
144
|
+
re.lastIndex = Math.max(re.lastIndex, argClose + 1);
|
|
145
|
+
}
|
|
146
|
+
return sites;
|
|
147
|
+
}
|
|
148
|
+
/** Weight of a bare python `assert` statement, classified by its operators. */
|
|
149
|
+
function pythonAssertWeight(stmt) {
|
|
150
|
+
if (stmt.includes("==") ||
|
|
151
|
+
stmt.includes("!=") ||
|
|
152
|
+
// Same tier rule (and same spelling tolerance) as the node-assert scanner
|
|
153
|
+
// — the two had drifted, scoring an identical schema check 3 in JS and 1
|
|
154
|
+
// in python when spelled checkSchema.
|
|
155
|
+
/\bcheck_?schema\s*\(/i.test(stmt))
|
|
156
|
+
return 3;
|
|
157
|
+
if (/\bre\s*\.\s*(?:match|fullmatch)\s*\(|\.startswith\s*\(|\bin\b/.test(stmt))
|
|
158
|
+
return 2;
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
161
|
+
function pythonAssertSites(raw, stripped) {
|
|
162
|
+
const sites = [];
|
|
163
|
+
const lines = stripped.split("\n");
|
|
164
|
+
let offset = 0;
|
|
165
|
+
for (let i = 0; i < lines.length; i++) {
|
|
166
|
+
const line = lines[i];
|
|
167
|
+
if (/^\s*assert\b/.test(line)) {
|
|
168
|
+
// Extend across paren/backslash continuations (bounded) so one multi-line
|
|
169
|
+
// assert counts once and fingerprints whole.
|
|
170
|
+
let stmt = line;
|
|
171
|
+
let last = i;
|
|
172
|
+
const depthOf = (s) => (s.match(/[([{]/g)?.length ?? 0) - (s.match(/[)\]}]/g)?.length ?? 0);
|
|
173
|
+
let depth = depthOf(stmt);
|
|
174
|
+
while ((depth > 0 || stmt.trimEnd().endsWith("\\")) &&
|
|
175
|
+
last + 1 < lines.length &&
|
|
176
|
+
last - i < 10) {
|
|
177
|
+
last++;
|
|
178
|
+
stmt += "\n" + lines[last];
|
|
179
|
+
depth = depthOf(stmt);
|
|
180
|
+
}
|
|
181
|
+
const stmtEnd = offset + stmt.length;
|
|
182
|
+
sites.push({
|
|
183
|
+
line: i + 1,
|
|
184
|
+
weight: pythonAssertWeight(stmt),
|
|
185
|
+
fingerprint: fingerprintOf(raw, offset + (line.length - line.trimStart().length), stmtEnd),
|
|
186
|
+
});
|
|
187
|
+
i = last;
|
|
188
|
+
offset = stmtEnd + 1;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
offset += line.length + 1;
|
|
192
|
+
}
|
|
193
|
+
return sites;
|
|
194
|
+
}
|
|
195
|
+
// Node's assert module — the idiom JS codegen actually emits
|
|
196
|
+
// (`assert.strictEqual(...)`, `assert.ok(checkSchema(...))`); Playwright
|
|
197
|
+
// `expect()` never appears in those files, so without this scanner every JS
|
|
198
|
+
// spec scored 0 sites and the strength gate false-failed on 0 > 0
|
|
199
|
+
// (found by running the verifier over the javascript/playwright goldens).
|
|
200
|
+
const NODE_ASSERT_RE = /(?<![\w$.])assert\s*\.\s*([\w$]+)\s*\(/g;
|
|
201
|
+
// Exact-value methods score like their expect() equivalents (toBe/toEqual=3);
|
|
202
|
+
// assert.match is a pattern check (2); everything else — ok, notStrictEqual
|
|
203
|
+
// (the "not null" idiom), throws — is existence-tier (1).
|
|
204
|
+
const NODE_ASSERT_EXACT = new Set([
|
|
205
|
+
"strictEqual",
|
|
206
|
+
"deepStrictEqual",
|
|
207
|
+
"equal",
|
|
208
|
+
"deepEqual",
|
|
209
|
+
]);
|
|
210
|
+
/** Shared regex-scan-and-fingerprint loop for call-shaped assertion dialects
|
|
211
|
+
* (node assert, java) — they differ only in the regex and the weighting. */
|
|
212
|
+
function callSites(raw, stripped, re, matcherOf, weightOf) {
|
|
213
|
+
const sites = [];
|
|
214
|
+
re.lastIndex = 0;
|
|
215
|
+
let m;
|
|
216
|
+
while ((m = re.exec(stripped)) !== null) {
|
|
217
|
+
const close = balancedCloseIndex(stripped, m.index + m[0].length - 1);
|
|
218
|
+
const args = close === -1 ? "" : stripped.slice(m.index + m[0].length, close);
|
|
219
|
+
sites.push({
|
|
220
|
+
line: lineOfOffset(stripped, m.index),
|
|
221
|
+
matcher: matcherOf(m),
|
|
222
|
+
weight: weightOf(m, args),
|
|
223
|
+
fingerprint: fingerprintOf(raw, startOfLine(raw, m.index), close === -1 ? m.index + m[0].length : close + 1),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return sites;
|
|
227
|
+
}
|
|
228
|
+
function nodeAssertSites(raw, stripped) {
|
|
229
|
+
return callSites(raw, stripped, NODE_ASSERT_RE, (m) => `assert.${m[1]}`,
|
|
230
|
+
// assert.ok(checkSchema(...)) is a full-shape check — exact tier, same
|
|
231
|
+
// treatment as check_schema in the python scanner.
|
|
232
|
+
(m, args) => NODE_ASSERT_EXACT.has(m[1])
|
|
233
|
+
? 3
|
|
234
|
+
: /\bcheck_?schema\s*\(/i.test(args)
|
|
235
|
+
? 3
|
|
236
|
+
: m[1] === "match"
|
|
237
|
+
? 2
|
|
238
|
+
: 1);
|
|
239
|
+
}
|
|
240
|
+
const JAVA_ASSERT_RE = /\b(assertEquals|assertNotEquals|assertTrue|assertFalse|assertNull|assertNotNull|assertThat|assertArrayEquals)\s*\(/g;
|
|
241
|
+
function javaAssertSites(raw, stripped) {
|
|
242
|
+
return callSites(raw, stripped, JAVA_ASSERT_RE, (m) => m[1], (m) => (m[1] === "assertEquals" || m[1] === "assertArrayEquals" ? 3 : 1));
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The assertion's SUBJECT — what it asserts about — recovered from a fingerprint
|
|
246
|
+
* (`expect(SUBJECT).matcher(...)` arg, or a python assert's left-hand side).
|
|
247
|
+
* Correlating subjects across baseline and current fingerprints is what tells a
|
|
248
|
+
* REPLACED assertion (subject still asserted, usually more strongly) from a
|
|
249
|
+
* REMOVED one (subject lost all coverage). Undefined when unrecoverable (e.g.
|
|
250
|
+
* truncated fingerprint) — callers treat that conservatively.
|
|
251
|
+
*/
|
|
252
|
+
export function subjectOf(fingerprint) {
|
|
253
|
+
const expectMatch = /\bexpect(?:\s*\.\s*soft)?\s*\(/.exec(fingerprint);
|
|
254
|
+
if (expectMatch) {
|
|
255
|
+
const openIdx = expectMatch.index + expectMatch[0].length - 1;
|
|
256
|
+
const close = balancedCloseIndex(fingerprint, openIdx);
|
|
257
|
+
if (close === -1)
|
|
258
|
+
return undefined;
|
|
259
|
+
return fingerprint.slice(openIdx + 1, close).replace(/\s+/g, "");
|
|
260
|
+
}
|
|
261
|
+
// Node assert: the subject is the first top-level argument
|
|
262
|
+
// (`assert.strictEqual(SUBJECT, expected, msg?)`, `assert.ok(SUBJECT, msg?)`).
|
|
263
|
+
const nodeAssert = /(?<![\w$.])assert\s*\.\s*[\w$]+\s*\(/.exec(fingerprint);
|
|
264
|
+
if (nodeAssert) {
|
|
265
|
+
const openIdx = nodeAssert.index + nodeAssert[0].length - 1;
|
|
266
|
+
const close = balancedCloseIndex(fingerprint, openIdx);
|
|
267
|
+
const args = fingerprint.slice(openIdx + 1, close === -1 ? undefined : close);
|
|
268
|
+
let depth = 0;
|
|
269
|
+
let end = args.length;
|
|
270
|
+
for (let i = 0; i < args.length; i++) {
|
|
271
|
+
const c = args[i];
|
|
272
|
+
if (c === "(" || c === "[" || c === "{")
|
|
273
|
+
depth++;
|
|
274
|
+
else if (c === ")" || c === "]" || c === "}")
|
|
275
|
+
depth--;
|
|
276
|
+
else if (depth === 0 && c === ",") {
|
|
277
|
+
end = i;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const subject = args.slice(0, end).replace(/\s+/g, "");
|
|
282
|
+
return subject.length > 0 ? subject : undefined;
|
|
283
|
+
}
|
|
284
|
+
const assertPrefix = /^assert\s+/.exec(fingerprint);
|
|
285
|
+
if (assertPrefix) {
|
|
286
|
+
// Depth-aware scan to the first TOP-LEVEL comparison operator, `is`/`in`,
|
|
287
|
+
// or comma (an assert message) — commas inside call args must not split.
|
|
288
|
+
const rest = fingerprint.slice(assertPrefix[0].length);
|
|
289
|
+
let depth = 0;
|
|
290
|
+
let end = rest.length;
|
|
291
|
+
for (let i = 0; i < rest.length; i++) {
|
|
292
|
+
const c = rest[i];
|
|
293
|
+
if (c === "(" || c === "[" || c === "{")
|
|
294
|
+
depth++;
|
|
295
|
+
else if (c === ")" || c === "]" || c === "}")
|
|
296
|
+
depth--;
|
|
297
|
+
else if (depth === 0) {
|
|
298
|
+
if (c === "," ||
|
|
299
|
+
c === "<" ||
|
|
300
|
+
c === ">" ||
|
|
301
|
+
(c === "=" && rest[i + 1] === "=") ||
|
|
302
|
+
(c === "!" && rest[i + 1] === "=") ||
|
|
303
|
+
rest.startsWith(" is ", i) ||
|
|
304
|
+
rest.startsWith(" in ", i)) {
|
|
305
|
+
end = i;
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const lhs = rest.slice(0, end).replace(/\s+/g, "");
|
|
311
|
+
return lhs.length > 0 ? lhs : undefined;
|
|
312
|
+
}
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
export function computeAssertionMetrics(content, language) {
|
|
316
|
+
let sites;
|
|
317
|
+
const { stripped } = strippedSources(content, language);
|
|
318
|
+
if (language === "python") {
|
|
319
|
+
sites = [
|
|
320
|
+
...pythonAssertSites(content, stripped),
|
|
321
|
+
...expectSites(content, stripped),
|
|
322
|
+
];
|
|
323
|
+
}
|
|
324
|
+
else if (language === "java") {
|
|
325
|
+
sites = javaAssertSites(content, stripped);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
sites = [
|
|
329
|
+
...expectSites(content, stripped),
|
|
330
|
+
...nodeAssertSites(content, stripped),
|
|
331
|
+
];
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
count: sites.length,
|
|
335
|
+
strengthScore: sites.reduce((sum, s) => sum + s.weight, 0),
|
|
336
|
+
fingerprints: sites
|
|
337
|
+
.slice(0, FINGERPRINT_SITE_CAP)
|
|
338
|
+
.map((s) => s.fingerprint),
|
|
339
|
+
sites,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Blanks `#` comments only, preserving string contents. */
|
|
2
|
+
export declare function stripPythonComments(src: string): string;
|
|
3
|
+
/** Blanks string literal contents only, preserving comments. */
|
|
4
|
+
export declare function stripPythonStrings(src: string): string;
|
|
5
|
+
/** Blanks both comments and strings. */
|
|
6
|
+
export declare function stripPythonCommentsAndStrings(src: string): string;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python twin of `src/utils/pom-scope/strip.ts`: one lexer-lite pass classifying
|
|
3
|
+
* `#` comments and string literals ('...', "...", '''...''', """...""", with
|
|
4
|
+
* prefixes like f/r/b treated as part of plain strings). Blanked characters
|
|
5
|
+
* become spaces (newlines preserved) so surviving code keeps its offsets.
|
|
6
|
+
*/
|
|
7
|
+
function classifySpans(src) {
|
|
8
|
+
const spans = [];
|
|
9
|
+
let i = 0;
|
|
10
|
+
const n = src.length;
|
|
11
|
+
while (i < n) {
|
|
12
|
+
const c = src[i];
|
|
13
|
+
if (c === "#") {
|
|
14
|
+
const start = i;
|
|
15
|
+
while (i < n && src[i] !== "\n")
|
|
16
|
+
i++;
|
|
17
|
+
spans.push({ kind: "comment", start, end: i });
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (c === "'" || c === '"') {
|
|
21
|
+
const quote = c;
|
|
22
|
+
const triple = src.startsWith(quote.repeat(3), i);
|
|
23
|
+
const closer = triple ? quote.repeat(3) : quote;
|
|
24
|
+
const start = i;
|
|
25
|
+
i += closer.length;
|
|
26
|
+
let closed = false;
|
|
27
|
+
while (i < n) {
|
|
28
|
+
if (src[i] === "\\" && i + 1 < n) {
|
|
29
|
+
i += 2;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (src.startsWith(closer, i)) {
|
|
33
|
+
i += closer.length;
|
|
34
|
+
closed = true;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
// Single-quoted strings can't span a raw newline; treat an unclosed
|
|
38
|
+
// opener as a stray quote rather than blanking to end of file.
|
|
39
|
+
if (src[i] === "\n" && !triple)
|
|
40
|
+
break;
|
|
41
|
+
i++;
|
|
42
|
+
}
|
|
43
|
+
if (!closed && !triple) {
|
|
44
|
+
i = start + 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
spans.push({ kind: "string", start, end: i });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
i++;
|
|
51
|
+
}
|
|
52
|
+
return spans;
|
|
53
|
+
}
|
|
54
|
+
function blankSpans(src, spans) {
|
|
55
|
+
if (spans.length === 0)
|
|
56
|
+
return src;
|
|
57
|
+
const out = src.split("");
|
|
58
|
+
for (const { start, end } of spans) {
|
|
59
|
+
for (let i = start; i < end; i++)
|
|
60
|
+
out[i] = src[i] === "\n" ? "\n" : " ";
|
|
61
|
+
}
|
|
62
|
+
return out.join("");
|
|
63
|
+
}
|
|
64
|
+
/** Blanks `#` comments only, preserving string contents. */
|
|
65
|
+
export function stripPythonComments(src) {
|
|
66
|
+
return blankSpans(src, classifySpans(src).filter((s) => s.kind === "comment"));
|
|
67
|
+
}
|
|
68
|
+
/** Blanks string literal contents only, preserving comments. */
|
|
69
|
+
export function stripPythonStrings(src) {
|
|
70
|
+
return blankSpans(src, classifySpans(src).filter((s) => s.kind === "string"));
|
|
71
|
+
}
|
|
72
|
+
/** Blanks both comments and strings. */
|
|
73
|
+
export function stripPythonCommentsAndStrings(src) {
|
|
74
|
+
return blankSpans(src, classifySpans(src));
|
|
75
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AssertionLanguage } from "./metrics.js";
|
|
2
|
+
/**
|
|
3
|
+
* The one place that knows how to strip a source per language — including the
|
|
4
|
+
* rule that JS/TS regex literals must be blanked BEFORE string stripping (an
|
|
5
|
+
* apostrophe inside a regex otherwise opens a phantom string span). Before
|
|
6
|
+
* this module, that rule was re-asserted by hand at seven call sites, and one
|
|
7
|
+
* verify pass re-stripped the same content 5–8 times; the one-entry memo makes
|
|
8
|
+
* repeated calls on the same content free (metrics + every lint module + the
|
|
9
|
+
* marker scan all run on one file per pass).
|
|
10
|
+
*/
|
|
11
|
+
export interface StrippedSources {
|
|
12
|
+
/** Comments blanked; strings intact. */
|
|
13
|
+
commentless: string;
|
|
14
|
+
/** Comments AND strings blanked (JS/TS: regex literals blanked first). */
|
|
15
|
+
stripped: string;
|
|
16
|
+
/** Strings blanked; comments intact (the marker scan reads comments). */
|
|
17
|
+
stringless: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function strippedSources(content: string, language: AssertionLanguage): StrippedSources;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { stripComments, stripCommentsAndStrings, stripStrings, } from "../pom-scope/strip.js";
|
|
2
|
+
import { stripPythonComments, stripPythonCommentsAndStrings, stripPythonStrings, } from "./python-strip.js";
|
|
3
|
+
import { blankJsRegexLiterals } from "./js-regex-blank.js";
|
|
4
|
+
let memoKeyContent;
|
|
5
|
+
let memoKeyLanguage;
|
|
6
|
+
let memoValue;
|
|
7
|
+
export function strippedSources(content, language) {
|
|
8
|
+
if (memoValue !== undefined &&
|
|
9
|
+
memoKeyContent === content &&
|
|
10
|
+
memoKeyLanguage === language) {
|
|
11
|
+
return memoValue;
|
|
12
|
+
}
|
|
13
|
+
let value;
|
|
14
|
+
if (language === "python") {
|
|
15
|
+
value = {
|
|
16
|
+
commentless: stripPythonComments(content),
|
|
17
|
+
stripped: stripPythonCommentsAndStrings(content),
|
|
18
|
+
stringless: stripPythonStrings(content),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
else if (language === "java") {
|
|
22
|
+
// Java gets the JS/TS comment/string grammar (same tokens) minus the
|
|
23
|
+
// regex-literal pre-pass, which does not exist in Java.
|
|
24
|
+
value = {
|
|
25
|
+
commentless: stripComments(content),
|
|
26
|
+
stripped: stripCommentsAndStrings(content),
|
|
27
|
+
stringless: stripStrings(content),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const blanked = blankJsRegexLiterals(content);
|
|
32
|
+
value = {
|
|
33
|
+
commentless: stripComments(content),
|
|
34
|
+
stripped: stripCommentsAndStrings(blanked),
|
|
35
|
+
stringless: stripStrings(blanked),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
memoKeyContent = content;
|
|
39
|
+
memoKeyLanguage = language;
|
|
40
|
+
memoValue = value;
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AssertionLanguage } from "./metrics.js";
|
|
2
|
+
import { type LintFinding, type LintOptions } from "./lint-types.js";
|
|
3
|
+
/** UI (Playwright) lints. Python (playwright-python) UI specs deliberately get
|
|
4
|
+
* the site-scoped lints only: the structural rules' patterns and accept-lists
|
|
5
|
+
* are JS/TS-specific (imports, waitForResponse shapes, expect matcher accept
|
|
6
|
+
* forms), and porting them without a python golden corpus to validate against
|
|
7
|
+
* would recreate the false-block class review round 2 removed. Pinned by test. */
|
|
8
|
+
export declare function lintUiSpec(raw: string, language: AssertionLanguage, opts?: LintOptions): LintFinding[];
|