@skyramp/mcp 0.4.2 → 0.4.3-rc.1
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/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 +24 -2
- package/build/recommendation/verifierContracts.js +32 -4
- package/build/recommendation/verifiers/changedLinesClaimed.d.ts +17 -0
- package/build/recommendation/verifiers/changedLinesClaimed.js +88 -0
- 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/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 +6 -0
- package/build/tools/code-refactor/reuse-record-store.d.ts +52 -0
- package/build/tools/code-refactor/reuse-record-store.js +126 -0
- package/build/tools/code-refactor/reuse-state.d.ts +18 -6
- package/build/tools/code-refactor/reuse-state.js +78 -131
- package/build/tools/code-refactor/utils-verify-gates.js +3 -3
- 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 +91 -29
- 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 +70 -22
- package/build/types/RepositoryAnalysis.d.ts +20 -20
- package/build/types/ReuseOutcome.d.ts +96 -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/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/head.d.ts +48 -0
- package/build/utils/utils-verify/head.js +115 -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/verify.d.ts +9 -3
- package/build/utils/utils-verify/verify.js +26 -12
- 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/traceRecordingBackend.js +303 -150
- 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.BAkLd5DX.js → index.ZYuEFHsi.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.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 +2 -2
- 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,14 +1,30 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import type
|
|
1
|
+
import { type SiblingScan } from "./call-sites.js";
|
|
2
|
+
import { type UtilsHelper } from "./parse.js";
|
|
3
3
|
import type { UtilsLanguageSpec } from "./language-spec.js";
|
|
4
|
-
/**
|
|
5
|
-
export interface
|
|
4
|
+
/** One helper with the Skyramp-generated tests that import it. */
|
|
5
|
+
export interface HelperImporters {
|
|
6
6
|
/** Absolute path of the utils file. */
|
|
7
7
|
file: string;
|
|
8
8
|
helper: string;
|
|
9
9
|
line: number;
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/** ABSOLUTE PATHS of the delivered tests importing it, in directory order — the
|
|
11
|
+
* spec under verification included. The count alone answers "is this helper
|
|
12
|
+
* shared"; the paths answer "which tests does a change to it reach", which is
|
|
13
|
+
* what a caller has to be named and run. */
|
|
14
|
+
importers: string[];
|
|
15
|
+
}
|
|
16
|
+
/** The importer walk's result. `read` is the walk's OWN verdict on whether it ran:
|
|
17
|
+
* an empty `helpers` means "no helper imports anything" only when `read` is true,
|
|
18
|
+
* and means nothing at all when it is false. A caller that blocks or asserts on
|
|
19
|
+
* the absence of an importer must consult it. */
|
|
20
|
+
export interface HelperImporterScan {
|
|
21
|
+
helpers: HelperImporters[];
|
|
22
|
+
/** Absolute paths of the generated tests the walk read. */
|
|
23
|
+
siblings: string[];
|
|
24
|
+
/** The sibling walk saw the whole directory: the listing succeeded and every
|
|
25
|
+
* candidate file was read. False means the importer lists are short by an unknown
|
|
26
|
+
* amount, not that they are empty. */
|
|
27
|
+
read: boolean;
|
|
12
28
|
}
|
|
13
29
|
/** The strings a sibling's source would contain if it imports the module at
|
|
14
30
|
* `modulePath` (relative to the sibling's directory): the path form for TS/JS
|
|
@@ -16,8 +32,21 @@ export interface SingleImporterHelper {
|
|
|
16
32
|
* `shared/X` → `shared.X` / `.shared.X`. */
|
|
17
33
|
export declare function moduleMentionCandidates(modulePath: string): string[];
|
|
18
34
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
35
|
+
* The Skyramp-generated tests in the spec's directory that import each utils helper —
|
|
36
|
+
* the spec under verification included. THE importer walk: the single-importer
|
|
37
|
+
* advisory and the changed-helper caller set both read it, so one definition decides
|
|
38
|
+
* what counts as an importing test.
|
|
39
|
+
*
|
|
40
|
+
* Fails open to an unread scan on any error. That is a silence, not an empty answer;
|
|
41
|
+
* `read` is what tells the two apart.
|
|
42
|
+
*
|
|
43
|
+
* SCOPE, which every caller inherits: siblings are the spec's OWN DIRECTORY. A
|
|
44
|
+
* generated test importing the module from another directory is not found here, so
|
|
45
|
+
* the importer list is a LOWER BOUND on the callers, never the complete set.
|
|
46
|
+
*/
|
|
47
|
+
export declare function helperImporters(testFile: string, utilsFiles: string[], helpersByFile: ReadonlyMap<string, UtilsHelper[]>, spec: UtilsLanguageSpec, preReadSiblings?: SiblingScan): Promise<HelperImporterScan>;
|
|
48
|
+
/**
|
|
49
|
+
* Helpers with 0 or 1 importing test — the advisory slice of {@link helperImporters}.
|
|
21
50
|
*
|
|
22
51
|
* Findings:
|
|
23
52
|
* - 0 importers: nothing imports the helper, not even the spec that wrote it — a
|
|
@@ -26,6 +55,26 @@ export declare function moduleMentionCandidates(modulePath: string): string[];
|
|
|
26
55
|
* TWO delivered tests: on a first run every helper has exactly one importer by
|
|
27
56
|
* construction (it seeds the utils file), so listing them would be guaranteed
|
|
28
57
|
* noise on the most common path.
|
|
29
|
-
*
|
|
58
|
+
*
|
|
59
|
+
* Reports NOTHING from an incomplete walk. This is a claim, not a gate — it tells the
|
|
60
|
+
* agent a helper is dead and to call it or delete it — and a walk that could not read
|
|
61
|
+
* every candidate cannot support it: the file it failed to read may be the importer.
|
|
62
|
+
* Silence on doubt, so the finding stays trustworthy when it is made.
|
|
63
|
+
*/
|
|
64
|
+
export declare function singleImporterHelpers(testFile: string, utilsFiles: string[], helpersByFile: ReadonlyMap<string, UtilsHelper[]>, spec: UtilsLanguageSpec, preReadSiblings?: SiblingScan): Promise<HelperImporters[]>;
|
|
65
|
+
/**
|
|
66
|
+
* `content` prepared for an import scan: the copy `importedNames` should read.
|
|
67
|
+
*
|
|
68
|
+
* Comments go in both languages — an import quoted in prose is not an import, and
|
|
69
|
+
* counting one makes a test a "caller" of a helper it cannot reach, which a gate then
|
|
70
|
+
* blocks on. Python string CONTENTS go too, since a Python import names no string, so
|
|
71
|
+
* the usage example in a docstring stops counting.
|
|
72
|
+
*
|
|
73
|
+
* TS/JS cannot blank every string, because the module specifier IS a string
|
|
74
|
+
* (`from "./skyrampUtils"`). Only the specifiers are kept: a string that directly
|
|
75
|
+
* follows `from` or `require(` stays, and every other string is blanked. Requiring
|
|
76
|
+
* the import to start a line is not enough on its own — a template literal holds
|
|
77
|
+
* whole lines, and the `require` form is bounded by a declaration keyword rather than
|
|
78
|
+
* by the line start — so quoted code kept reading as executable code.
|
|
30
79
|
*/
|
|
31
|
-
export declare function
|
|
80
|
+
export declare function importScanSource(content: string, spec: UtilsLanguageSpec): string;
|
|
@@ -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). */
|