@skyramp/mcp 0.4.2 → 0.4.3-rc.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/build/execution/wrapperConfig.d.ts +4 -0
- package/build/execution/wrapperConfig.js +14 -2
- package/build/prompts/code-reuse.d.ts +5 -1
- package/build/prompts/code-reuse.js +16 -10
- package/build/prompts/modularization/ui-test-modularization.js +9 -2
- package/build/prompts/pom-aware-code-reuse.d.ts +4 -1
- package/build/prompts/pom-aware-code-reuse.js +38 -6
- package/build/prompts/reuse-hand-off.d.ts +4 -0
- package/build/prompts/reuse-hand-off.js +10 -5
- package/build/prompts/shared-helper-policy.d.ts +5 -0
- package/build/prompts/shared-helper-policy.js +5 -0
- package/build/prompts/test-maintenance/driftAnalysisShared.js +2 -0
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +2 -1
- package/build/prompts/testbot/testbot-prompts.js +33 -16
- package/build/recommendation/answers.d.ts +5 -3
- package/build/recommendation/answers.js +5 -2
- package/build/recommendation/fakeVerifyContext.d.ts +3 -0
- package/build/recommendation/fakeVerifyContext.js +16 -0
- package/build/recommendation/runVerifiers.js +2 -0
- package/build/recommendation/types.d.ts +12 -0
- package/build/recommendation/verifierContracts.d.ts +32 -2
- package/build/recommendation/verifierContracts.js +45 -4
- package/build/recommendation/verifiers/changedLinesClaimed.d.ts +17 -0
- package/build/recommendation/verifiers/changedLinesClaimed.js +88 -0
- package/build/recommendation/verifiers/coverage.js +143 -4
- package/build/recommendation/verifiers/expectedValueSourced.js +8 -0
- package/build/recommendation/verifiers/requirementSourced.js +141 -11
- package/build/services/TestGenerationService.js +4 -0
- package/build/tools/code-refactor/caller-gate.d.ts +105 -0
- package/build/tools/code-refactor/caller-gate.js +174 -0
- package/build/tools/code-refactor/changed-helper-report.d.ts +62 -0
- package/build/tools/code-refactor/changed-helper-report.js +114 -0
- package/build/tools/code-refactor/codeReuseTool.js +131 -62
- package/build/tools/code-refactor/enhanceAssertionsTool.js +2 -2
- package/build/tools/code-refactor/helper-callers.d.ts +65 -0
- package/build/tools/code-refactor/helper-callers.js +85 -0
- package/build/tools/code-refactor/modularizationTool.js +10 -0
- package/build/tools/code-refactor/pom-pass-state.d.ts +51 -0
- package/build/tools/code-refactor/pom-pass-state.js +115 -0
- package/build/tools/code-refactor/removed-modules.d.ts +33 -0
- package/build/tools/code-refactor/removed-modules.js +61 -0
- package/build/tools/code-refactor/retrofit-state.d.ts +3 -2
- package/build/tools/code-refactor/retrofit-state.js +11 -12
- package/build/tools/code-refactor/reuse-outcome.d.ts +33 -1
- package/build/tools/code-refactor/reuse-record-store.d.ts +57 -0
- package/build/tools/code-refactor/reuse-record-store.js +140 -0
- package/build/tools/code-refactor/reuse-state.d.ts +34 -9
- package/build/tools/code-refactor/reuse-state.js +140 -135
- package/build/tools/code-refactor/utils-verify-gates.js +42 -18
- package/build/tools/code-refactor/verify-gates.d.ts +4 -0
- package/build/tools/code-refactor/verify-gates.js +8 -1
- package/build/tools/generate-tests/generateContractRestTool.js +7 -7
- package/build/tools/generate-tests/generateIntegrationRestTool.js +8 -8
- package/build/tools/generate-tests/generateUIRestTool.js +3 -3
- package/build/tools/submitReportTool.js +121 -30
- package/build/tools/test-management/actionsTool.js +7 -0
- package/build/tools/test-management/registerTestPlanTool.d.ts +1 -0
- package/build/tools/test-management/registerTestPlanTool.js +71 -23
- package/build/types/RepositoryAnalysis.d.ts +20 -20
- package/build/types/ReuseOutcome.d.ts +123 -0
- package/build/types/TestExecution.d.ts +11 -0
- package/build/types/TestExecution.js +19 -0
- package/build/types/index.d.ts +1 -1
- package/build/utils/branchDiff.d.ts +1 -1
- package/build/utils/branchDiff.js +1 -1
- package/build/utils/changedRuns.d.ts +13 -0
- package/build/utils/changedRuns.js +56 -0
- package/build/utils/featureFlags.d.ts +8 -10
- package/build/utils/featureFlags.js +23 -21
- package/build/utils/normalizeSkyrampImports.d.ts +4 -1
- package/build/utils/normalizeSkyrampImports.js +24 -17
- package/build/utils/pom-scope/ownership.d.ts +19 -0
- package/build/utils/pom-scope/ownership.js +31 -0
- package/build/utils/pom-scope/pom-files.d.ts +8 -0
- package/build/utils/pom-scope/pom-files.js +8 -0
- package/build/utils/pom-scope/scoring.js +13 -1
- package/build/utils/pom-scope/strip.d.ts +10 -0
- package/build/utils/pom-scope/strip.js +9 -0
- package/build/utils/pom-verify/verify.js +13 -2
- package/build/utils/reuseRouting.d.ts +30 -2
- package/build/utils/reuseRouting.js +36 -15
- package/build/utils/testDependencyPolicy.js +4 -16
- package/build/utils/utils-verify/action-sites.d.ts +2 -2
- package/build/utils/utils-verify/allow.d.ts +1 -1
- package/build/utils/utils-verify/allow.js +15 -0
- package/build/utils/utils-verify/body-reach.d.ts +2 -2
- package/build/utils/utils-verify/body-reach.js +4 -1
- package/build/utils/utils-verify/call-sites.d.ts +16 -6
- package/build/utils/utils-verify/call-sites.js +12 -6
- package/build/utils/utils-verify/changed-helpers.d.ts +38 -0
- package/build/utils/utils-verify/changed-helpers.js +91 -0
- package/build/utils/utils-verify/changed-lines.d.ts +48 -0
- package/build/utils/utils-verify/changed-lines.js +106 -0
- package/build/utils/utils-verify/head.d.ts +73 -0
- package/build/utils/utils-verify/head.js +156 -0
- package/build/utils/utils-verify/importers.d.ts +59 -10
- package/build/utils/utils-verify/importers.js +121 -31
- package/build/utils/utils-verify/in-house.js +26 -31
- package/build/utils/utils-verify/index.d.ts +3 -0
- package/build/utils/utils-verify/index.js +3 -0
- package/build/utils/utils-verify/language-spec.js +10 -2
- package/build/utils/utils-verify/locate.d.ts +3 -0
- package/build/utils/utils-verify/locate.js +6 -6
- package/build/utils/utils-verify/parse.d.ts +13 -1
- package/build/utils/utils-verify/parse.js +122 -49
- package/build/utils/utils-verify/retrofit-equivalence.d.ts +0 -5
- package/build/utils/utils-verify/retrofit-equivalence.js +2 -34
- package/build/utils/utils-verify/stage.js +4 -16
- package/build/utils/utils-verify/typecheck.d.ts +25 -4
- package/build/utils/utils-verify/typecheck.js +95 -13
- package/build/utils/utils-verify/verify.d.ts +33 -8
- package/build/utils/utils-verify/verify.js +180 -18
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +1 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +6 -0
- package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +4 -2
- package/node_modules/playwright/lib/mcp/skyramp/snapshotLocators.js +246 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +378 -237
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +21 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-LNgEKtdV.js → codeMirrorModule-Bou8kKzE.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-Bwr1eMKC.js → defaultSettingsView-DTp2-8Si.js} +3 -3
- 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/{index.BAkLd5DX.js → index.uiPTFncY.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.BPopbasy.js → uiMode.GLqsx5cI.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/pollingRecorderSource.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +3 -3
- package/plugin/prompts/code-reuse/hand-off.md +13 -1
- package/plugin/prompts/plan-tests.md +2 -2
- package/plugin/skills/enhance-assertions/reference/shared-rules.md +1 -0
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
2
|
import { generatedSiblings } from "./call-sites.js";
|
|
3
|
+
import { realpath } from "./locate.js";
|
|
4
|
+
import { blankComments, blankCommentsAndStrings, } from "./parse.js";
|
|
5
|
+
import { stringSpans } from "../pom-scope/strip.js";
|
|
3
6
|
/** The strings a sibling's source would contain if it imports the module at
|
|
4
7
|
* `modulePath` (relative to the sibling's directory): the path form for TS/JS
|
|
5
8
|
* specifiers, and the dotted Python form — `../shared/X` → `..shared.X`,
|
|
@@ -13,29 +16,36 @@ export function moduleMentionCandidates(modulePath) {
|
|
|
13
16
|
return [posix, "./" + posix, rest, dotted, "." + rest];
|
|
14
17
|
}
|
|
15
18
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
19
|
+
* The Skyramp-generated tests in the spec's directory that import each utils helper —
|
|
20
|
+
* the spec under verification included. THE importer walk: the single-importer
|
|
21
|
+
* advisory and the changed-helper caller set both read it, so one definition decides
|
|
22
|
+
* what counts as an importing test.
|
|
18
23
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* noise on the most common path.
|
|
26
|
-
* Fails open to "no findings" on any read error.
|
|
24
|
+
* Fails open to an unread scan on any error. That is a silence, not an empty answer;
|
|
25
|
+
* `read` is what tells the two apart.
|
|
26
|
+
*
|
|
27
|
+
* SCOPE, which every caller inherits: siblings are the spec's OWN DIRECTORY. A
|
|
28
|
+
* generated test importing the module from another directory is not found here, so
|
|
29
|
+
* the importer list is a LOWER BOUND on the callers, never the complete set.
|
|
27
30
|
*/
|
|
28
|
-
export async function
|
|
31
|
+
export async function helperImporters(testFile, utilsFiles, helpersByFile, spec, preReadSiblings) {
|
|
29
32
|
let dir;
|
|
30
33
|
let siblings;
|
|
31
34
|
try {
|
|
32
|
-
|
|
35
|
+
// CANONICAL, because the module paths this is compared against are: they come
|
|
36
|
+
// from locateUtilsFiles, which realpaths everything. A spec named through a
|
|
37
|
+
// symlinked parent (macOS `/var` → `/private/var`, and every worktree under a
|
|
38
|
+
// linked temporary directory) otherwise relativises to a `../../…` path no
|
|
39
|
+
// sibling's import can contain, so every helper in the module read as imported
|
|
40
|
+
// by nothing — silently, since an empty importer list is also the honest answer
|
|
41
|
+
// for a helper nobody calls.
|
|
42
|
+
dir = path.dirname(await realpath(testFile));
|
|
33
43
|
siblings =
|
|
34
44
|
preReadSiblings ??
|
|
35
45
|
(await generatedSiblings(testFile, spec, { includeSelf: true }));
|
|
36
46
|
}
|
|
37
47
|
catch {
|
|
38
|
-
return [];
|
|
48
|
+
return { helpers: [], siblings: [], read: false };
|
|
39
49
|
}
|
|
40
50
|
// `importedNames` matches by basename stem, so a bare `from SkyrampUtils import …`
|
|
41
51
|
// in a sibling would also be credited to `../shared/SkyrampUtils`. A sibling's
|
|
@@ -49,30 +59,110 @@ export async function singleImporterHelpers(testFile, utilsFiles, helpersByFile,
|
|
|
49
59
|
inDir: path.dirname(f) === dir,
|
|
50
60
|
mentions: moduleMentionCandidates(path.relative(dir, f).replace(/\.[^.]+$/, "")),
|
|
51
61
|
}));
|
|
52
|
-
// helper key = `${utilsFile}\0${name}` →
|
|
53
|
-
const
|
|
62
|
+
// helper key = `${utilsFile}\0${name}` → the tests importing it
|
|
63
|
+
const importers = new Map();
|
|
54
64
|
for (const [file, helpers] of helpersByFile)
|
|
55
65
|
for (const h of helpers)
|
|
56
|
-
|
|
57
|
-
for (const { content } of siblings) {
|
|
66
|
+
importers.set(`${file}\0${h.name}`, []);
|
|
67
|
+
for (const { file: sibling, content } of siblings.files) {
|
|
68
|
+
// Imports are read from the CODE, never from prose. A docstring or a comment
|
|
69
|
+
// quoting `from SkyrampUtils import …` would otherwise make that test a caller of
|
|
70
|
+
// a helper it cannot reach, and the gate would refuse the report over it — a
|
|
71
|
+
// false block, which this design treats as the worst outcome available to it.
|
|
72
|
+
const code = importScanSource(content, spec);
|
|
58
73
|
for (const { file: utilsFile, stem, inDir, mentions } of stems) {
|
|
59
74
|
if (!inDir && !mentions.some((c) => content.includes(c)))
|
|
60
75
|
continue;
|
|
61
|
-
const imported = new Set(spec.importedNames(
|
|
76
|
+
const imported = new Set(spec.importedNames(code, stem));
|
|
62
77
|
for (const h of helpersByFile.get(utilsFile) ?? [])
|
|
63
|
-
if (imported.has(h.name))
|
|
64
|
-
|
|
65
|
-
counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
66
|
-
}
|
|
78
|
+
if (imported.has(h.name))
|
|
79
|
+
importers.get(`${utilsFile}\0${h.name}`)?.push(sibling);
|
|
67
80
|
}
|
|
68
81
|
}
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
const helpers = [];
|
|
83
|
+
for (const [file, hs] of helpersByFile)
|
|
84
|
+
for (const h of hs)
|
|
85
|
+
helpers.push({
|
|
86
|
+
file,
|
|
87
|
+
helper: h.name,
|
|
88
|
+
line: h.line,
|
|
89
|
+
importers: importers.get(`${file}\0${h.name}`) ?? [],
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
helpers,
|
|
93
|
+
siblings: siblings.files.map((s) => s.file),
|
|
94
|
+
read: siblings.complete,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Helpers with 0 or 1 importing test — the advisory slice of {@link helperImporters}.
|
|
99
|
+
*
|
|
100
|
+
* Findings:
|
|
101
|
+
* - 0 importers: nothing imports the helper, not even the spec that wrote it — a
|
|
102
|
+
* dead helper, actionable on any run.
|
|
103
|
+
* - 1 importer: not (yet) shared. Reported only when the directory holds at least
|
|
104
|
+
* TWO delivered tests: on a first run every helper has exactly one importer by
|
|
105
|
+
* construction (it seeds the utils file), so listing them would be guaranteed
|
|
106
|
+
* noise on the most common path.
|
|
107
|
+
*
|
|
108
|
+
* Reports NOTHING from an incomplete walk. This is a claim, not a gate — it tells the
|
|
109
|
+
* agent a helper is dead and to call it or delete it — and a walk that could not read
|
|
110
|
+
* every candidate cannot support it: the file it failed to read may be the importer.
|
|
111
|
+
* Silence on doubt, so the finding stays trustworthy when it is made.
|
|
112
|
+
*/
|
|
113
|
+
export async function singleImporterHelpers(testFile, utilsFiles, helpersByFile, spec, preReadSiblings) {
|
|
114
|
+
const scan = await helperImporters(testFile, utilsFiles, helpersByFile, spec, preReadSiblings);
|
|
115
|
+
if (!scan.read)
|
|
116
|
+
return [];
|
|
117
|
+
const secondRunExists = scan.siblings.length >= 2;
|
|
118
|
+
return scan.helpers.filter((h) => h.importers.length === 0 || (h.importers.length === 1 && secondRunExists));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* `content` prepared for an import scan: the copy `importedNames` should read.
|
|
122
|
+
*
|
|
123
|
+
* Comments go in both languages — an import quoted in prose is not an import, and
|
|
124
|
+
* counting one makes a test a "caller" of a helper it cannot reach, which a gate then
|
|
125
|
+
* blocks on. Python string CONTENTS go too, since a Python import names no string, so
|
|
126
|
+
* the usage example in a docstring stops counting.
|
|
127
|
+
*
|
|
128
|
+
* TS/JS cannot blank every string, because the module specifier IS a string
|
|
129
|
+
* (`from "./skyrampUtils"`). Only the specifiers are kept: a string that directly
|
|
130
|
+
* follows `from` or `require(` stays, and every other string is blanked. Requiring
|
|
131
|
+
* the import to start a line is not enough on its own — a template literal holds
|
|
132
|
+
* whole lines, and the `require` form is bounded by a declaration keyword rather than
|
|
133
|
+
* by the line start — so quoted code kept reading as executable code.
|
|
134
|
+
*/
|
|
135
|
+
export function importScanSource(content, spec) {
|
|
136
|
+
if (spec.language === "python")
|
|
137
|
+
return blankCommentsAndStrings(content, spec);
|
|
138
|
+
const noComments = blankComments(content, spec);
|
|
139
|
+
const out = noComments.split("");
|
|
140
|
+
for (const { start, end } of stringSpans(noComments)) {
|
|
141
|
+
// A module specifier is a string in exactly one position: straight after `from`
|
|
142
|
+
// or after `require(`. Anything else is data, whatever it spells.
|
|
143
|
+
if (isSpecifierPosition(noComments, start))
|
|
144
|
+
continue;
|
|
145
|
+
for (let i = start; i < end; i++)
|
|
146
|
+
out[i] = noComments[i] === "\n" ? "\n" : " ";
|
|
147
|
+
}
|
|
148
|
+
return out.join("");
|
|
149
|
+
}
|
|
150
|
+
/** Whether the string opening at `at` sits where a module specifier does: after
|
|
151
|
+
* `from`, or after `require(`, with only whitespace between. Read backwards over a
|
|
152
|
+
* fixed window rather than by matching a regex against `src.slice(0, at)` — that
|
|
153
|
+
* copied the file once per literal, quadratic in a spec with thousands of them. */
|
|
154
|
+
function isSpecifierPosition(src, at) {
|
|
155
|
+
let i = at;
|
|
156
|
+
while (i > 0 && /\s/.test(src[i - 1]))
|
|
157
|
+
i--;
|
|
158
|
+
const word = (w) => src.slice(Math.max(0, i - w.length), i) === w &&
|
|
159
|
+
!/[\w$]/.test(src[i - w.length - 1] ?? "");
|
|
160
|
+
if (word("from"))
|
|
161
|
+
return true;
|
|
162
|
+
if (src[i - 1] !== "(")
|
|
163
|
+
return false;
|
|
164
|
+
i--;
|
|
165
|
+
while (i > 0 && /\s/.test(src[i - 1]))
|
|
166
|
+
i--;
|
|
167
|
+
return word("require");
|
|
78
168
|
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import { execFile } from "child_process";
|
|
2
|
-
import { promisify } from "util";
|
|
3
1
|
import * as fs from "fs";
|
|
4
2
|
import * as path from "path";
|
|
5
3
|
import { detectGitRoot } from "../gitStaging.js";
|
|
6
4
|
import { logger } from "../logger.js";
|
|
7
5
|
import { escapeRegExp } from "../regex.js";
|
|
8
|
-
import { importedModules, realpath
|
|
6
|
+
import { hasUtilsHeaderContent, importedModules, realpath } from "./locate.js";
|
|
7
|
+
import { contentAtHead } from "./head.js";
|
|
9
8
|
import { blankComments, blankCommentsAndStrings, defaultAt, hasDefault, parseUtilsFile, } from "./parse.js";
|
|
10
9
|
import { blankStringContents, matchingClose } from "./action-key.js";
|
|
11
|
-
const execFileAsync = promisify(execFile);
|
|
12
10
|
export async function locateInHouseModules(params) {
|
|
13
11
|
const root = params.cwd ?? detectGitRoot(params.testFile);
|
|
14
12
|
if (!root)
|
|
@@ -26,7 +24,7 @@ export async function locateInHouseModules(params) {
|
|
|
26
24
|
const rel = path.relative(realRoot, real);
|
|
27
25
|
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel))
|
|
28
26
|
continue;
|
|
29
|
-
const head = await
|
|
27
|
+
const head = await contentAtHead(real, realRoot);
|
|
30
28
|
if (head === undefined)
|
|
31
29
|
continue; // not tracked: created this run, or never a file
|
|
32
30
|
// Ownership is read at HEAD and nowhere else. A Skyramp module from an earlier
|
|
@@ -34,31 +32,12 @@ export async function locateInHouseModules(params) {
|
|
|
34
32
|
// did to its first lines; a module headerless at HEAD is the customer's, and a
|
|
35
33
|
// header the working copy gained since is a change the additive gate refuses —
|
|
36
34
|
// reading the working copy here would let that stamp reclassify their file.
|
|
37
|
-
if (
|
|
35
|
+
if (hasUtilsHeaderContent(head))
|
|
38
36
|
continue;
|
|
39
37
|
out.push({ file: real, relPath: rel.split(path.sep).join("/") });
|
|
40
38
|
}
|
|
41
39
|
return out.sort((a, b) => a.file.localeCompare(b.file));
|
|
42
40
|
}
|
|
43
|
-
/** Whether a module's first lines carry the Skyramp utils header. */
|
|
44
|
-
function hasHeader(content) {
|
|
45
|
-
return UTILS_FILE_HEADER_RE.test(content.split("\n").slice(0, 5).join("\n"));
|
|
46
|
-
}
|
|
47
|
-
/** The module's content at HEAD, or `undefined` when HEAD does not hold it (or git
|
|
48
|
-
* cannot be asked). */
|
|
49
|
-
async function headContent(root, rel) {
|
|
50
|
-
try {
|
|
51
|
-
const { stdout } = await execFileAsync("git", ["show", `HEAD:${rel}`], {
|
|
52
|
-
cwd: root,
|
|
53
|
-
encoding: "utf8",
|
|
54
|
-
maxBuffer: 16 * 1024 * 1024,
|
|
55
|
-
});
|
|
56
|
-
return stdout;
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
41
|
/** The number of arguments a helper accepts: from the position of its last required
|
|
63
42
|
* parameter to its total, a rest parameter opening the top. A parameter with a
|
|
64
43
|
* default, an optional `x?`, or a Python `*args`/`**kw` is not required — but a
|
|
@@ -224,20 +203,36 @@ function definitionLineOf(moduleLines, name) {
|
|
|
224
203
|
const re = new RegExp(`\\b(?:class|const|let|var|enum|interface|type|function|def)\\s+${escapeRegExp(name)}\\b`);
|
|
225
204
|
return moduleLines.find((l) => re.test(l));
|
|
226
205
|
}
|
|
227
|
-
/** `export { name } from "…"`,
|
|
228
|
-
*
|
|
206
|
+
/** `export { name } from "…"`, `export * from "…"`, or a binding this module IMPORTS
|
|
207
|
+
* and then lists in a local `export { … }` — a binding of another module exported
|
|
208
|
+
* here. Exists as an export; not readable as a helper of this module. The last form
|
|
209
|
+
* is how a fixtures module passes Playwright's `expect` through beside its extended
|
|
210
|
+
* `test`: `import { test as base, expect } from "@playwright/test"; … export { expect };`. */
|
|
229
211
|
function isReExported(moduleCode, name) {
|
|
230
212
|
// `export * from "./x"` may legally provide any name; nothing here can read it.
|
|
231
213
|
if (/\bexport\s*\*\s*from\b/.test(moduleCode))
|
|
232
214
|
return true;
|
|
233
|
-
for (const m of moduleCode.matchAll(/\bexport\s*\{([^}]*)\}\s*from\b
|
|
215
|
+
for (const m of moduleCode.matchAll(/\bexport\s*\{([^}]*)\}(\s*from\b)?/g))
|
|
234
216
|
for (const raw of m[1].split(",")) {
|
|
235
217
|
const [local, exported] = raw.trim().split(/\s+as\s+/);
|
|
236
|
-
if ((exported ?? local)
|
|
218
|
+
if ((exported ?? local) !== name)
|
|
219
|
+
continue;
|
|
220
|
+
// `export { x } from "…"` names the other module's binding directly; a local
|
|
221
|
+
// list exports this module's binding, which is another module's only when
|
|
222
|
+
// an import brought it in.
|
|
223
|
+
if (m[2] || isImportedBinding(moduleCode, local))
|
|
237
224
|
return true;
|
|
238
225
|
}
|
|
239
226
|
return false;
|
|
240
227
|
}
|
|
228
|
+
/** Whether an import statement or a CommonJS destructure binds `local` in this
|
|
229
|
+
* module: `import { local }`, `import { x as local }`, `import local from`,
|
|
230
|
+
* `import * as local from`, `const { local } = require(…)`. */
|
|
231
|
+
function isImportedBinding(moduleCode, local) {
|
|
232
|
+
const n = escapeRegExp(local);
|
|
233
|
+
return (new RegExp(`\\bimport\\s+(?:type\\s+)?[^;]*?(?:[{,]\\s*(?:\\w+\\s+as\\s+)?${n}\\s*[,}]|\\*\\s+as\\s+${n}\\b|\\b${n}\\s*(?:,|\\bfrom\\b))[^;]*?\\bfrom\\b`).test(moduleCode) ||
|
|
234
|
+
new RegExp(`\\b(?:const|let|var)\\s*\\{[^}]*\\b${n}\\b[^}]*\\}\\s*=\\s*require\\s*\\(`).test(moduleCode));
|
|
235
|
+
}
|
|
241
236
|
function importLine(lines, name) {
|
|
242
237
|
const re = new RegExp(`\\b${escapeRegExp(name)}\\b`);
|
|
243
238
|
const i = lines.findIndex((l) => /\b(?:import|require|from)\b/.test(l) && re.test(l));
|
|
@@ -306,7 +301,7 @@ export async function additiveChanges(file, root, spec) {
|
|
|
306
301
|
duplicates: [],
|
|
307
302
|
};
|
|
308
303
|
const rel = path.relative(root, file).split(path.sep).join("/");
|
|
309
|
-
const head = await
|
|
304
|
+
const head = await contentAtHead(file, root);
|
|
310
305
|
if (head === undefined) {
|
|
311
306
|
logger.warning("Could not read an in-house module at HEAD — the additive gate did not run", { file });
|
|
312
307
|
return result;
|
|
@@ -335,7 +330,7 @@ export async function additiveChanges(file, root, spec) {
|
|
|
335
330
|
}
|
|
336
331
|
result.ran = true;
|
|
337
332
|
// Rule 5 of the design, enforced: the file stays the customer's file.
|
|
338
|
-
if (
|
|
333
|
+
if (hasUtilsHeaderContent(work) && !hasUtilsHeaderContent(head))
|
|
339
334
|
result.violations.push({
|
|
340
335
|
kind: "in-house-modified",
|
|
341
336
|
file,
|
|
@@ -2,9 +2,12 @@ export * from "./language-spec.js";
|
|
|
2
2
|
export * from "./parse.js";
|
|
3
3
|
export * from "./allow.js";
|
|
4
4
|
export * from "./locate.js";
|
|
5
|
+
export * from "./head.js";
|
|
5
6
|
export * from "./verify.js";
|
|
6
7
|
export * from "./stage.js";
|
|
7
8
|
export * from "./call-sites.js";
|
|
9
|
+
export * from "./importers.js";
|
|
10
|
+
export * from "./changed-helpers.js";
|
|
8
11
|
export * from "./action-sites.js";
|
|
9
12
|
export * from "./retrofit-equivalence.js";
|
|
10
13
|
export * from "./in-house.js";
|
|
@@ -2,9 +2,12 @@ export * from "./language-spec.js";
|
|
|
2
2
|
export * from "./parse.js";
|
|
3
3
|
export * from "./allow.js";
|
|
4
4
|
export * from "./locate.js";
|
|
5
|
+
export * from "./head.js";
|
|
5
6
|
export * from "./verify.js";
|
|
6
7
|
export * from "./stage.js";
|
|
7
8
|
export * from "./call-sites.js";
|
|
9
|
+
export * from "./importers.js";
|
|
10
|
+
export * from "./changed-helpers.js";
|
|
8
11
|
export * from "./action-sites.js";
|
|
9
12
|
export * from "./retrofit-equivalence.js";
|
|
10
13
|
export * from "./in-house.js";
|
|
@@ -115,7 +115,10 @@ function tsModuleQualifiers(spec, stem) {
|
|
|
115
115
|
const stemRe = stem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
116
116
|
const from = `["'][^"']*\\/${stemRe}(?:\\.[cm]?[jt]s)?["']`;
|
|
117
117
|
const out = new Set();
|
|
118
|
-
|
|
118
|
+
// Anchored to the START of a line, for the same reason the named-import scan is: a
|
|
119
|
+
// TypeScript specifier is itself a string, so the scan cannot blank strings to keep
|
|
120
|
+
// prose out — it has to require the import to BE a statement.
|
|
121
|
+
for (const m of spec.matchAll(new RegExp(`^\\s*import\\s*\\*\\s*as\\s+([A-Za-z_$][\\w$]*)\\s*from\\s*${from}`, "gm")))
|
|
119
122
|
out.add(m[1]);
|
|
120
123
|
for (const m of spec.matchAll(new RegExp(`(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*require\\(\\s*${from}\\s*\\)`, "g")))
|
|
121
124
|
out.add(m[1]);
|
|
@@ -125,7 +128,12 @@ function tsImportedNames(spec, stem) {
|
|
|
125
128
|
const names = new Set();
|
|
126
129
|
const stemRe = stem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
127
130
|
const from = `["'][^"']*\\/${stemRe}(?:\\.[cm]?[jt]s)?["']`;
|
|
128
|
-
|
|
131
|
+
// Anchored to the START of a line: a TypeScript specifier is itself a string, so
|
|
132
|
+
// the scan cannot blank strings to keep prose out — it has to require the import to
|
|
133
|
+
// BE a statement. Without this, ordinary data (`const sample = 'import { x } from
|
|
134
|
+
// "./skyrampUtils"'`) counted as an import, and a gate reading importers as callers
|
|
135
|
+
// would block a report over a test that cannot reach the helper.
|
|
136
|
+
for (const m of spec.matchAll(new RegExp(`^\\s*import\\s*\\{([^}]*)\\}\\s*from\\s*${from}`, "gm"))) {
|
|
129
137
|
for (const raw of m[1].split(",")) {
|
|
130
138
|
// The EXPORTED name (before `as`): the count checks what the utils file defines,
|
|
131
139
|
// so an alias would under-count. Same choice as the Python `from … import x as y`.
|
|
@@ -3,6 +3,9 @@ import { type UtilsLanguageSpec } from "./language-spec.js";
|
|
|
3
3
|
* <timestamp>`. Deliberately NOT the bare `SKYRAMP_UTILS_HEADER`, which every generated
|
|
4
4
|
* TEST also carries as its codegen marker (`Generated by Skyramp v<version>`). */
|
|
5
5
|
export declare const UTILS_FILE_HEADER_RE: RegExp;
|
|
6
|
+
/** Whether `content`'s first lines carry the utils header — the test that makes a
|
|
7
|
+
* file Skyramp's shared module, for a caller that already holds the content. */
|
|
8
|
+
export declare function hasUtilsHeaderContent(content: string): boolean;
|
|
6
9
|
export interface LocateUtilsParams {
|
|
7
10
|
/** Absolute path of the delivered test file. */
|
|
8
11
|
testFile: string;
|
|
@@ -15,6 +15,11 @@ export const UTILS_FILE_HEADER_RE = new RegExp(`${SKYRAMP_UTILS_HEADER.replace(/
|
|
|
15
15
|
const HEADER_SCAN_LINES = 5;
|
|
16
16
|
/** Enough bytes for five header lines; a utils file is never read whole to find them. */
|
|
17
17
|
const HEADER_SCAN_BYTES = 2048;
|
|
18
|
+
/** Whether `content`'s first lines carry the utils header — the test that makes a
|
|
19
|
+
* file Skyramp's shared module, for a caller that already holds the content. */
|
|
20
|
+
export function hasUtilsHeaderContent(content) {
|
|
21
|
+
return UTILS_FILE_HEADER_RE.test(content.split("\n").slice(0, HEADER_SCAN_LINES).join("\n"));
|
|
22
|
+
}
|
|
18
23
|
/**
|
|
19
24
|
* Every shared utils file relevant to `testFile`, by CONTENT, never by name.
|
|
20
25
|
*
|
|
@@ -130,12 +135,7 @@ export async function hasUtilsHeader(file) {
|
|
|
130
135
|
fh = await fs.promises.open(file, "r");
|
|
131
136
|
const buf = Buffer.alloc(HEADER_SCAN_BYTES);
|
|
132
137
|
const { bytesRead } = await fh.read(buf, 0, HEADER_SCAN_BYTES, 0);
|
|
133
|
-
|
|
134
|
-
.toString("utf8", 0, bytesRead)
|
|
135
|
-
.split("\n")
|
|
136
|
-
.slice(0, HEADER_SCAN_LINES)
|
|
137
|
-
.join("\n");
|
|
138
|
-
return UTILS_FILE_HEADER_RE.test(head);
|
|
138
|
+
return hasUtilsHeaderContent(buf.toString("utf8", 0, bytesRead));
|
|
139
139
|
}
|
|
140
140
|
catch (err) {
|
|
141
141
|
// A missing file is the common, silent case. Anything else means a file exists
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { type UtilsLanguageSpec } from "./language-spec.js";
|
|
2
2
|
import { type HelperSleep } from "./fixed-sleep.js";
|
|
3
|
+
import { type SourceSpan } from "../pom-scope/strip.js";
|
|
3
4
|
export interface HelperAssertion {
|
|
4
5
|
line: number;
|
|
5
6
|
text: string;
|
|
6
7
|
}
|
|
7
8
|
export interface UtilsHelper {
|
|
8
9
|
name: string;
|
|
9
|
-
/** 1-based line of the definition
|
|
10
|
+
/** 1-based line of the definition — the `def`/`function` line, which is where a
|
|
11
|
+
* finding is reported. */
|
|
10
12
|
line: number;
|
|
13
|
+
/** 1-based line where the definition's SOURCE begins: the first line of its
|
|
14
|
+
* decorator stack, or `line` when it has none. A decorator is part of what the
|
|
15
|
+
* helper does — `@retry(3)` against `@retry(1)` changes every call site — so a
|
|
16
|
+
* reader comparing implementations has to start here, while a reader pointing at
|
|
17
|
+
* the helper still uses `line`. */
|
|
18
|
+
startLine: number;
|
|
11
19
|
/** 1-based line of the last body line (inclusive). */
|
|
12
20
|
endLine: number;
|
|
13
21
|
method?: string;
|
|
@@ -104,6 +112,10 @@ export declare function parseUtilsFile(content: string, spec: UtilsLanguageSpec)
|
|
|
104
112
|
/** True when the `path` value is a single string literal ending the argument — a
|
|
105
113
|
* concatenation (`"/api/orders/" + str(id) + "/items"`) has no route this can name. */
|
|
106
114
|
export declare function isSingleLiteral(line: string, m: RegExpExecArray): boolean;
|
|
115
|
+
/** The string literals in `src`, quotes included, in order. TS/JS go through the
|
|
116
|
+
* shared scanner; Python goes through the one below, so a caller sees one definition
|
|
117
|
+
* of a literal for each language. */
|
|
118
|
+
export declare function stringSpansOf(src: string, spec: UtilsLanguageSpec): SourceSpan[];
|
|
107
119
|
/** Comments and string CONTENTS blanked (quotes kept, line count preserved). Python
|
|
108
120
|
* comments are `#…`; TS/JS go through the shared stripper. */
|
|
109
121
|
/** String CONTENTS blanked (quotes kept, comments kept, line count preserved). */
|
|
@@ -7,7 +7,7 @@ function actionSequence(body, language) {
|
|
|
7
7
|
return keys ? { actionKey: keys.join("; "), actionSteps: keys } : {};
|
|
8
8
|
}
|
|
9
9
|
import { fixedSleepsIn, pythonSleepBindings, } from "./fixed-sleep.js";
|
|
10
|
-
import { stripComments, stripCommentsAndStrings, stripStrings, } from "../pom-scope/strip.js";
|
|
10
|
+
import { stringSpans, stripComments, stripCommentsAndStrings, stripStrings, } from "../pom-scope/strip.js";
|
|
11
11
|
/** Whether the `<` at `i` opens a generic (`Record<string, …>`, `Array<{…}>`) rather
|
|
12
12
|
* than comparing (`i < 3`): a generic follows an identifier or `>` and is followed
|
|
13
13
|
* by a type start; a comparison is followed by a number, a space and a value, or
|
|
@@ -266,6 +266,7 @@ export function parseUtilsFile(content, spec) {
|
|
|
266
266
|
const helper = {
|
|
267
267
|
name,
|
|
268
268
|
line: start + 1,
|
|
269
|
+
startLine: decoratorStart(lines, structure, start) + 1,
|
|
269
270
|
endLine: Math.max(end, start + 1),
|
|
270
271
|
wrapsRequest: false,
|
|
271
272
|
hasGuard: hasPageErrorGuard(body.join("\n"), spec),
|
|
@@ -355,69 +356,100 @@ export function isSingleLiteral(line, m) {
|
|
|
355
356
|
after.startsWith(")") ||
|
|
356
357
|
after.startsWith("}"));
|
|
357
358
|
}
|
|
358
|
-
/**
|
|
359
|
-
*
|
|
360
|
-
|
|
361
|
-
export function
|
|
362
|
-
|
|
363
|
-
return stripStrings(src);
|
|
364
|
-
return blankPythonStrings(src);
|
|
359
|
+
/** The string literals in `src`, quotes included, in order. TS/JS go through the
|
|
360
|
+
* shared scanner; Python goes through the one below, so a caller sees one definition
|
|
361
|
+
* of a literal for each language. */
|
|
362
|
+
export function stringSpansOf(src, spec) {
|
|
363
|
+
return spec.language === "python" ? pythonStringSpans(src) : stringSpans(src);
|
|
365
364
|
}
|
|
366
365
|
/**
|
|
367
|
-
*
|
|
366
|
+
* The string literals in Python source, quotes included.
|
|
368
367
|
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
368
|
+
* Triple quotes are tested BEFORE single ones, and this is the whole reason the scan
|
|
369
|
+
* is not a regex: a `"""` read one quote at a time matches an empty `""` and leaves
|
|
370
|
+
* the payload to be treated as code. A multiline payload's own indentation is then
|
|
371
|
+
* read as formatting, and a change to it disappears.
|
|
372
|
+
*
|
|
373
|
+
* A single-quoted string that never closes before the end of its line is a stray
|
|
374
|
+
* apostrophe in code (`# don't`, or a regex), not an opener — the same rule the TS
|
|
375
|
+
* scanner applies. A prefix (`r`, `f`, `b`, `rb`) is code; only the quote opens the
|
|
376
|
+
* literal.
|
|
377
|
+
*
|
|
378
|
+
* COMMENTS ARE SKIPPED while looking for an opener, in the same pass rather than a
|
|
379
|
+
* prior one: a comment that merely mentions a quote (`# a docstring starts with """`)
|
|
380
|
+
* would otherwise open a span that runs to the next real delimiter, blanking every
|
|
381
|
+
* definition in between so its helpers vanish from every check that reads the
|
|
382
|
+
* structure. A `#` inside a literal is not a comment, which is why one pass has to
|
|
383
|
+
* decide both.
|
|
373
384
|
*/
|
|
374
|
-
function
|
|
375
|
-
const out =
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
let i = 0;
|
|
382
|
-
const n = src.length;
|
|
383
|
-
while (i < n) {
|
|
384
|
-
const c = src[i];
|
|
385
|
-
// A comment runs to the end of the line and holds no string opener.
|
|
386
|
-
if (c === "#") {
|
|
387
|
-
while (i < n && src[i] !== "\n")
|
|
385
|
+
function pythonStringSpans(src) {
|
|
386
|
+
const out = [];
|
|
387
|
+
for (let i = 0; i < src.length; i++) {
|
|
388
|
+
const q = src[i];
|
|
389
|
+
if (q === "#") {
|
|
390
|
+
// Outside a literal by construction — a literal advances `i` past its end.
|
|
391
|
+
while (i < src.length && src[i] !== "\n")
|
|
388
392
|
i++;
|
|
389
393
|
continue;
|
|
390
394
|
}
|
|
391
|
-
if (
|
|
392
|
-
i++;
|
|
395
|
+
if (q !== '"' && q !== "'")
|
|
393
396
|
continue;
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
while (i < n) {
|
|
402
|
-
if (src[i] === "\\") {
|
|
403
|
-
i += 2;
|
|
397
|
+
const triple = src.startsWith(q + q + q, i);
|
|
398
|
+
const close = triple ? q + q + q : q;
|
|
399
|
+
let j = i + close.length;
|
|
400
|
+
let end = -1;
|
|
401
|
+
while (j < src.length) {
|
|
402
|
+
if (src[j] === "\\") {
|
|
403
|
+
j += 2;
|
|
404
404
|
continue;
|
|
405
405
|
}
|
|
406
|
-
if (src.startsWith(
|
|
407
|
-
|
|
408
|
-
closed = true;
|
|
406
|
+
if (src.startsWith(close, j)) {
|
|
407
|
+
end = j + close.length;
|
|
409
408
|
break;
|
|
410
409
|
}
|
|
411
410
|
// Only a triple-quoted literal may hold a raw newline.
|
|
412
|
-
if (src[
|
|
411
|
+
if (src[j] === "\n" && !triple)
|
|
413
412
|
break;
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
if (!closed) {
|
|
417
|
-
i = start + 1;
|
|
418
|
-
continue;
|
|
413
|
+
j++;
|
|
419
414
|
}
|
|
420
|
-
|
|
415
|
+
if (end === -1)
|
|
416
|
+
continue; // never closed: not a literal
|
|
417
|
+
out.push({ start: i, end });
|
|
418
|
+
i = end - 1;
|
|
419
|
+
}
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
422
|
+
/** Comments and string CONTENTS blanked (quotes kept, line count preserved). Python
|
|
423
|
+
* comments are `#…`; TS/JS go through the shared stripper. */
|
|
424
|
+
/** String CONTENTS blanked (quotes kept, comments kept, line count preserved). */
|
|
425
|
+
export function blankStrings(src, spec) {
|
|
426
|
+
if (spec.language !== "python")
|
|
427
|
+
return stripStrings(src);
|
|
428
|
+
return blankPythonStrings(src);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* String CONTENTS blanked in Python source, quotes kept, offsets and line count
|
|
432
|
+
* preserved.
|
|
433
|
+
*
|
|
434
|
+
* Reads its literals from `pythonStringSpans` — the same scan `stringSpansOf` serves —
|
|
435
|
+
* so ONE definition decides what a Python literal is, for the callers that blank a
|
|
436
|
+
* literal and the callers that must copy one through unchanged.
|
|
437
|
+
*
|
|
438
|
+
* A line-by-line regex cannot do this. A `"""…"""` block spans lines, so a `#` inside a
|
|
439
|
+
* docstring or an embedded SQL block read as a comment, and every scanner built on this
|
|
440
|
+
* then treated quoted text as code the file executes — the delivery-time marker strip
|
|
441
|
+
* deleted a line out of a value rather than out of a comment, and a `def` at column 0
|
|
442
|
+
* inside a docstring ended a helper early and reappeared as a second definition of the
|
|
443
|
+
* same name.
|
|
444
|
+
*/
|
|
445
|
+
function blankPythonStrings(src) {
|
|
446
|
+
const out = src.split("");
|
|
447
|
+
for (const { start, end } of pythonStringSpans(src)) {
|
|
448
|
+
// The quote delimiters stay: callers read this copy for offsets and expect a
|
|
449
|
+
// literal to still look like one.
|
|
450
|
+
const q = src.startsWith(src[start].repeat(3), start) ? 3 : 1;
|
|
451
|
+
for (let i = start + q; i < end - q; i++)
|
|
452
|
+
out[i] = src[i] === "\n" ? "\n" : " ";
|
|
421
453
|
}
|
|
422
454
|
return out.join("");
|
|
423
455
|
}
|
|
@@ -508,6 +540,47 @@ function signatureLines(body) {
|
|
|
508
540
|
}
|
|
509
541
|
return out;
|
|
510
542
|
}
|
|
543
|
+
/** The index of the first line of the decorator stack above `start`, or `start` when
|
|
544
|
+
* there is none. Comments and blank lines may sit between decorators; anything else
|
|
545
|
+
* ends the stack.
|
|
546
|
+
*
|
|
547
|
+
* A decorator may span lines — `@retry(` … `)` — so the walk goes up by EXPRESSION,
|
|
548
|
+
* not by line: from a line that closes more brackets than it opens, it keeps going
|
|
549
|
+
* until the brackets balance, and only then asks whether that line starts with `@`.
|
|
550
|
+
* A line-at-a-time walk sees `)` above the definition, stops there, and drops the
|
|
551
|
+
* whole decorator out of the comparison.
|
|
552
|
+
*
|
|
553
|
+
* Brackets are counted on `structure`, the copy with comments and string contents
|
|
554
|
+
* blanked, for the same reason every other structural read here uses it: a bracket
|
|
555
|
+
* typed inside a string (`retry_if_message(")")`) is not a bracket. The `@` itself is
|
|
556
|
+
* read from `lines`, where it is still legible. Both copies keep the same line count,
|
|
557
|
+
* so one index walks both.
|
|
558
|
+
*
|
|
559
|
+
* Language-neutral: a definition with no `@` line simply gets `start` back. */
|
|
560
|
+
function decoratorStart(lines, structure, start) {
|
|
561
|
+
let first = start;
|
|
562
|
+
let depth = 0;
|
|
563
|
+
for (let i = start - 1; i >= 0; i--) {
|
|
564
|
+
const l = lines[i].trim();
|
|
565
|
+
// Blank, or a comment: `structure` has comments blanked already, so the test
|
|
566
|
+
// is one and the same in every language — no prefix list to keep in step.
|
|
567
|
+
if (depth === 0 && (structure[i] ?? "").trim() === "")
|
|
568
|
+
continue;
|
|
569
|
+
for (const ch of structure[i] ?? "") {
|
|
570
|
+
if (")]}".includes(ch))
|
|
571
|
+
depth++;
|
|
572
|
+
else if ("([{".includes(ch))
|
|
573
|
+
depth--;
|
|
574
|
+
}
|
|
575
|
+
if (depth > 0)
|
|
576
|
+
continue; // inside a wrapped expression; keep walking up
|
|
577
|
+
depth = 0;
|
|
578
|
+
if (!l.startsWith("@"))
|
|
579
|
+
break;
|
|
580
|
+
first = i;
|
|
581
|
+
}
|
|
582
|
+
return first;
|
|
583
|
+
}
|
|
511
584
|
/** Whether the def at `start` carries `@overload` / `@typing.overload` anywhere in
|
|
512
585
|
* its decorator stack (comments and other decorators may sit between). */
|
|
513
586
|
function isOverloadStub(lines, start) {
|