@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
|
@@ -2,10 +2,12 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { logger } from "../logger.js";
|
|
4
4
|
import { locateUtilsFiles } from "./locate.js";
|
|
5
|
-
import { parseUtilsFile } from "./parse.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { blankCommentsAndStrings, countAssertions, hasPageErrorGuard, parseUtilsFile, } from "./parse.js";
|
|
6
|
+
import { escapeRegExp } from "../regex.js";
|
|
7
|
+
import { findSiblingInlineCallSites, generatedSiblings, } from "./call-sites.js";
|
|
8
|
+
import { parseUtilsAllows, } from "./allow.js";
|
|
9
|
+
import { singleImporterHelpers, } from "./importers.js";
|
|
10
|
+
import { helperFamilyFor, helperFamilyOf, scenarioNameViolation, utilsSpecFor, utilsSpecForFile, } from "./language-spec.js";
|
|
9
11
|
/**
|
|
10
12
|
* Deterministic post-reuse check of the shared utils file(s) a spec depends on.
|
|
11
13
|
*
|
|
@@ -42,10 +44,11 @@ export async function verifyUtils(params) {
|
|
|
42
44
|
advisories: [],
|
|
43
45
|
malformedAllows: [],
|
|
44
46
|
spec,
|
|
45
|
-
family,
|
|
47
|
+
fallbackFamily: family,
|
|
46
48
|
inlineCallSites: [],
|
|
49
|
+
singleImporters: [],
|
|
47
50
|
};
|
|
48
|
-
if (!spec
|
|
51
|
+
if (!spec)
|
|
49
52
|
return result;
|
|
50
53
|
let specContent = "";
|
|
51
54
|
try {
|
|
@@ -54,8 +57,14 @@ export async function verifyUtils(params) {
|
|
|
54
57
|
catch {
|
|
55
58
|
/* the spec may be absent outside a run; import counting then reports 0 */
|
|
56
59
|
}
|
|
60
|
+
if (spec.language === "python")
|
|
61
|
+
result.violations.push(...importFormViolations(params.testFile, specContent, utilsFiles));
|
|
57
62
|
const allHelpers = [];
|
|
63
|
+
const helpersByFile = new Map();
|
|
64
|
+
const allAllows = [];
|
|
58
65
|
const importedNames = new Set();
|
|
66
|
+
const importedByFile = new Map();
|
|
67
|
+
const helpersOfFile = new Map();
|
|
59
68
|
// Route map spans every utils file: the reuse prompt allows the helpers to split
|
|
60
69
|
// across files, and the same method+path in two of them is still one duplicate.
|
|
61
70
|
const byRoute = new Map();
|
|
@@ -73,16 +82,36 @@ export async function verifyUtils(params) {
|
|
|
73
82
|
});
|
|
74
83
|
throw err; // the caller records a verify error, which fails OPEN — never a silent PASS
|
|
75
84
|
}
|
|
76
|
-
const helpers = parseUtilsFile(content, spec
|
|
85
|
+
const helpers = parseUtilsFile(content, spec);
|
|
86
|
+
for (const h of helpers) {
|
|
87
|
+
// An untyped parameter is an error under `noImplicitAny`; whether the host repo's
|
|
88
|
+
// tsconfig reaches the tests directory is its call, so this advises and never blocks.
|
|
89
|
+
if (h.untypedParams.length > 0)
|
|
90
|
+
result.advisories.push({
|
|
91
|
+
kind: "untyped-parameter",
|
|
92
|
+
file,
|
|
93
|
+
helper: h.name,
|
|
94
|
+
line: h.line,
|
|
95
|
+
// Names only — the remediation is stated once, in the advisory footer.
|
|
96
|
+
detail: h.untypedParams.map((p) => `\`${p}\``).join(", "),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
77
99
|
allHelpers.push(...helpers);
|
|
100
|
+
helpersByFile.set(file, helpers);
|
|
101
|
+
helpersOfFile.set(file, helpers);
|
|
78
102
|
const { allows, malformed } = parseUtilsAllows(content, spec);
|
|
103
|
+
allAllows.push(...allows);
|
|
79
104
|
result.helpers += helpers.length;
|
|
80
105
|
result.malformedAllows.push(...malformed.map((line) => ({ file, line })));
|
|
81
106
|
const stem = path.basename(file).replace(/\.[^.]+$/, "");
|
|
82
107
|
const defined = new Set(helpers.map((h) => h.name));
|
|
108
|
+
const importedHere = new Set();
|
|
83
109
|
for (const n of spec.importedNames(specContent, stem))
|
|
84
|
-
if (defined.has(n))
|
|
110
|
+
if (defined.has(n)) {
|
|
85
111
|
importedNames.add(n);
|
|
112
|
+
importedHere.add(n);
|
|
113
|
+
}
|
|
114
|
+
importedByFile.set(file, importedHere);
|
|
86
115
|
// One marker documents ONE violation of that kind in that helper: a second,
|
|
87
116
|
// undocumented scenario claim in the same helper is not covered by the first
|
|
88
117
|
// decline. Markers are consumed in order.
|
|
@@ -91,7 +120,7 @@ export async function verifyUtils(params) {
|
|
|
91
120
|
const k = `${a.kind}\0${a.helper}`;
|
|
92
121
|
budget.set(k, (budget.get(k) ?? 0) + 1);
|
|
93
122
|
}
|
|
94
|
-
for (const v of violationsIn(helpers, file, family, byRoute)) {
|
|
123
|
+
for (const v of violationsIn(helpers, file, family, spec, byRoute)) {
|
|
95
124
|
const k = `${v.kind}\0${v.helper}`;
|
|
96
125
|
const left = budget.get(k) ?? 0;
|
|
97
126
|
if (left > 0) {
|
|
@@ -107,6 +136,51 @@ export async function verifyUtils(params) {
|
|
|
107
136
|
}
|
|
108
137
|
}
|
|
109
138
|
result.imported = importedNames.size;
|
|
139
|
+
// One directory walk feeds both advisories: every sibling is read exactly once.
|
|
140
|
+
let siblings = [];
|
|
141
|
+
try {
|
|
142
|
+
siblings = await generatedSiblings(params.testFile, spec, {
|
|
143
|
+
includeSelf: true,
|
|
144
|
+
});
|
|
145
|
+
result.singleImporters = await singleImporterHelpers(params.testFile, utilsFiles, helpersByFile, spec, siblings);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
/* advisory only — never fails a verify */
|
|
149
|
+
}
|
|
150
|
+
if (params.baseline && specContent) {
|
|
151
|
+
// Delivered assertions live in the spec and in the helpers it imports — counted
|
|
152
|
+
// per utils FILE (a same-named helper in another module is not this import) and
|
|
153
|
+
// per CALL SITE on a comment/string-blanked copy of the spec: the baseline
|
|
154
|
+
// counted executions in source order, so a 2-assertion block extracted from 3
|
|
155
|
+
// repetitions delivers 6, and a commented-out call delivers nothing.
|
|
156
|
+
const blankedSpec = blankCommentsAndStrings(specContent, spec);
|
|
157
|
+
let inHelpers = 0;
|
|
158
|
+
// The guard may legitimately move INTO a shared helper — registered there,
|
|
159
|
+
// asserted in the spec, it still guards. Credited exactly like assertions.
|
|
160
|
+
let guardInHelpers = false;
|
|
161
|
+
for (const [file, imported] of importedByFile) {
|
|
162
|
+
for (const h of helpersOfFile.get(file) ?? []) {
|
|
163
|
+
if (!imported.has(h.name))
|
|
164
|
+
continue;
|
|
165
|
+
const calls = (blankedSpec.match(new RegExp(`\\b${escapeRegExp(h.name)}\\s*\\(`, "g")) ?? []).length;
|
|
166
|
+
if (calls === 0)
|
|
167
|
+
continue;
|
|
168
|
+
inHelpers += calls * h.assertions.length;
|
|
169
|
+
if (h.hasGuard)
|
|
170
|
+
guardInHelpers = true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
for (const v of baselineViolations(params.baseline, params.testFile, spec, specContent, inHelpers, guardInHelpers)) {
|
|
174
|
+
// A marker in any utils file documents an assertion-loss (never a removed
|
|
175
|
+
// guard). At most one loss finding exists per verify, so a matching marker's
|
|
176
|
+
// presence is the whole budget — nothing to consume.
|
|
177
|
+
if (v.kind === "assertion-loss" &&
|
|
178
|
+
allAllows.some((a) => a.kind === v.kind && a.helper === v.helper))
|
|
179
|
+
result.allowed.push(v);
|
|
180
|
+
else
|
|
181
|
+
result.violations.push(v);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
110
184
|
result.ok =
|
|
111
185
|
result.violations.length === 0 && result.malformedAllows.length === 0;
|
|
112
186
|
// Integration only — the same gate as the prompt rule. `contract`, `load` and an
|
|
@@ -115,7 +189,7 @@ export async function verifyUtils(params) {
|
|
|
115
189
|
// not have taken.
|
|
116
190
|
if ((params.testType ?? "").toLowerCase() === "integration") {
|
|
117
191
|
try {
|
|
118
|
-
result.inlineCallSites = await findSiblingInlineCallSites(params.testFile, utilsFiles, allHelpers, spec);
|
|
192
|
+
result.inlineCallSites = await findSiblingInlineCallSites(params.testFile, utilsFiles, allHelpers, spec, siblings);
|
|
119
193
|
}
|
|
120
194
|
catch {
|
|
121
195
|
/* advisory only — never fails a verify */
|
|
@@ -123,9 +197,147 @@ export async function verifyUtils(params) {
|
|
|
123
197
|
}
|
|
124
198
|
return result;
|
|
125
199
|
}
|
|
126
|
-
|
|
200
|
+
/** The baseline (A7/A8) findings for a delivered spec — pure; marker handling is the
|
|
201
|
+
* caller's. Runs whether or not a utils file was located: deleting the utils file
|
|
202
|
+
* (with the moved assertions and guard in it) must not read as "nothing to verify". */
|
|
203
|
+
function baselineViolations(baseline, testFile, spec, specContent, inHelpers, guardInHelpers) {
|
|
204
|
+
const found = [];
|
|
205
|
+
const inSpec = countAssertions(specContent, spec);
|
|
206
|
+
const delivered = inSpec + inHelpers;
|
|
207
|
+
// Marker grammar takes an identifier: `orders.spec.ts` → `orders_spec_ts`.
|
|
208
|
+
const stem = path.basename(testFile).replace(/\W/g, "_");
|
|
209
|
+
if (delivered < baseline.assertions)
|
|
210
|
+
found.push({
|
|
211
|
+
kind: "assertion-loss",
|
|
212
|
+
file: testFile,
|
|
213
|
+
helper: stem,
|
|
214
|
+
line: 1,
|
|
215
|
+
detail: `${baseline.assertions} assertions at the modularization hand-out, ${delivered} delivered (${inSpec} in the test, ${inHelpers} in the helpers it imports and calls) — restore each removed check in the test body after the helper call it verifies, or document a deliberate consolidation with the allow marker`,
|
|
216
|
+
});
|
|
217
|
+
if (baseline.pageErrorGuard &&
|
|
218
|
+
!guardInHelpers &&
|
|
219
|
+
!hasPageErrorGuard(specContent, spec))
|
|
220
|
+
found.push({
|
|
221
|
+
kind: "guard-removed",
|
|
222
|
+
file: testFile,
|
|
223
|
+
helper: stem,
|
|
224
|
+
line: 1,
|
|
225
|
+
detail: `the page.on('pageerror', …) guard present at the modularization hand-out is gone — restore the registration (in this spec, or in a shared helper this spec imports and calls) and its final expect over the collected errors; it is never removed and cannot be documented away`,
|
|
226
|
+
});
|
|
227
|
+
return found;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Python only. The import form that works here is not the one that works in the
|
|
231
|
+
* customer's repo, and the difference is invisible in this run.
|
|
232
|
+
*
|
|
233
|
+
* pytest imports a test inside a package directory (one carrying `__init__.py`) from
|
|
234
|
+
* that package's first NON-package ancestor, so a bare `from SkyrampUtils import …`
|
|
235
|
+
* resolves only when the runner puts the test's own directory on `sys.path`. The
|
|
236
|
+
* Skyramp executor flattens its bundle and drops the `__init__.py` files, so it does;
|
|
237
|
+
* the customer running `pytest` from their repo root does not, and the test dies at
|
|
238
|
+
* collection with `ModuleNotFoundError`, taking the whole file with it. Measured on
|
|
239
|
+
* prefect (`tests/`, `tests/server/`, `tests/server/api/` all packages) and present in
|
|
240
|
+
* five of the six real Python repos in the eval corpus — the majority shape, not an
|
|
241
|
+
* edge case.
|
|
242
|
+
*
|
|
243
|
+
* The relative form is not simply the answer: under the flattened bundle it fails with
|
|
244
|
+
* `attempted relative import with no known parent package`. The two layouts want
|
|
245
|
+
* opposite forms, so a package directory must carry BOTH — the relative import first,
|
|
246
|
+
* falling back to the bare one:
|
|
247
|
+
*
|
|
248
|
+
* try:
|
|
249
|
+
* from .SkyrampUtils import create_order
|
|
250
|
+
* except ImportError: # flattened test bundle (no package context)
|
|
251
|
+
* from SkyrampUtils import create_order
|
|
252
|
+
*
|
|
253
|
+
* Verified in both layouts. Outside a package the bare form alone is correct, and the
|
|
254
|
+
* fallback would be noise, so it is required only where `__init__.py` sits beside the
|
|
255
|
+
* spec — which is exactly where pytest changes its rule.
|
|
256
|
+
*/
|
|
257
|
+
export function importFormViolations(testFile, specContent, utilsFiles) {
|
|
258
|
+
const dir = path.dirname(testFile);
|
|
259
|
+
if (!fs.existsSync(path.join(dir, "__init__.py")))
|
|
260
|
+
return [];
|
|
261
|
+
const out = [];
|
|
262
|
+
// Docstrings and other triple-quoted text can carry an example import; blanked
|
|
263
|
+
// (length- and line-preserving) so an example is never read as a live import.
|
|
264
|
+
const lines = blankTripleQuoted(specContent).split("\n");
|
|
265
|
+
for (const file of utilsFiles) {
|
|
266
|
+
// Only a module beside the spec is addressable relatively.
|
|
267
|
+
if (path.dirname(file) !== dir)
|
|
268
|
+
continue;
|
|
269
|
+
const stem = path.basename(file).replace(/\.[^.]+$/, "");
|
|
270
|
+
const esc = escapeRegExp(stem);
|
|
271
|
+
// Both supported spellings: `from M import a, b` and `import M as alias`.
|
|
272
|
+
const bareFrom = new RegExp(`^\\s*from\\s+${esc}\\s+import\\b(.*)$`);
|
|
273
|
+
const bareModule = new RegExp(`^\\s*import\\s+${esc}\\b(.*)$`);
|
|
274
|
+
const relFrom = new RegExp(`^\\s*from\\s+\\.${esc}\\s+import\\b`);
|
|
275
|
+
const relModule = new RegExp(`^\\s*from\\s+\\.\\s+import\\s+${esc}\\b`);
|
|
276
|
+
lines.forEach((text, i) => {
|
|
277
|
+
const from = bareFrom.exec(text);
|
|
278
|
+
const mod = from ? null : bareModule.exec(text);
|
|
279
|
+
if (!from && !mod)
|
|
280
|
+
return;
|
|
281
|
+
// The bare form is legitimate ONLY as the `except ImportError` arm of a pair
|
|
282
|
+
// whose relative import comes first. Checked per import against what precedes
|
|
283
|
+
// IT — a relative import elsewhere in the file (or of other names) does not
|
|
284
|
+
// make this line a fallback, and a bare import above the pair still runs first
|
|
285
|
+
// and still fails collection.
|
|
286
|
+
const before = lines.slice(0, i);
|
|
287
|
+
const lastIndexWhere = (pred) => {
|
|
288
|
+
for (let k = before.length - 1; k >= 0; k--)
|
|
289
|
+
if (pred(before[k]))
|
|
290
|
+
return k;
|
|
291
|
+
return -1;
|
|
292
|
+
};
|
|
293
|
+
// ANY except clause counts, not just `except ImportError`. `except
|
|
294
|
+
// ModuleNotFoundError` (the error actually raised, and an ImportError
|
|
295
|
+
// subclass) and `except (ImportError, ModuleNotFoundError)` are both things a
|
|
296
|
+
// careful author writes, and this gate REFUSES THE REPORT — so the structural
|
|
297
|
+
// requirement below (a relative import first, this import indented inside the
|
|
298
|
+
// handler) is what carries the meaning. Narrowing on the exception name buys
|
|
299
|
+
// no detection and costs blocked runs on correct code.
|
|
300
|
+
const exceptAt = lastIndexWhere((l) => /^\s*except\b/.test(l));
|
|
301
|
+
// Either relative spelling satisfies either bare spelling: a pair mixing
|
|
302
|
+
// `from . import M` with `from M import x` is unusual but not wrong, and
|
|
303
|
+
// refusing it would again block correct code.
|
|
304
|
+
const relAt = lastIndexWhere((l) => relFrom.test(l) || relModule.test(l));
|
|
305
|
+
// Textual order is not enough: the bare import must sit INSIDE that except
|
|
306
|
+
// suite. `except ImportError: pass` followed by a top-level bare import passes
|
|
307
|
+
// an ordering test, still runs, and still dies at collection.
|
|
308
|
+
const indentOf = (l) => l.length - l.trimStart().length;
|
|
309
|
+
const insideExcept = exceptAt !== -1 &&
|
|
310
|
+
indentOf(text) > indentOf(lines[exceptAt]) &&
|
|
311
|
+
lines
|
|
312
|
+
.slice(exceptAt + 1, i)
|
|
313
|
+
.every((l) => l.trim() === "" || indentOf(l) > indentOf(lines[exceptAt]));
|
|
314
|
+
if (insideExcept && relAt !== -1 && relAt < exceptAt)
|
|
315
|
+
return;
|
|
316
|
+
const names = (from ? from[1] : (mod?.[1] ?? "")).trim();
|
|
317
|
+
const fixed = from
|
|
318
|
+
? `from .${stem} import ${names}`
|
|
319
|
+
: `from . import ${stem}${names ? " " + names : ""}`;
|
|
320
|
+
out.push({
|
|
321
|
+
kind: "import-form",
|
|
322
|
+
file: testFile,
|
|
323
|
+
helper: stem,
|
|
324
|
+
line: i + 1,
|
|
325
|
+
detail: `\`${text.trim()}\` — this directory is a package (\`__init__.py\`), so pytest resolves imports from the package root and will NOT find \`${stem}\` on sys.path when the customer runs it; the test dies at collection. ` +
|
|
326
|
+
`Replace it with the pair that works in both layouts: \`try:\` / \` ${fixed}\` / \`except ImportError:\` / \` ${text.trim()}\``,
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return out;
|
|
331
|
+
}
|
|
332
|
+
/** Triple-quoted regions blanked, preserving length and line count, so an example
|
|
333
|
+
* import inside a docstring is never read as a live one. */
|
|
334
|
+
function blankTripleQuoted(src) {
|
|
335
|
+
return src.replace(/("""|''')[\s\S]*?\1/g, (m) => m.replace(/[^\n]/g, " "));
|
|
336
|
+
}
|
|
337
|
+
function violationsIn(helpers, file, fallback, spec, byRoute) {
|
|
127
338
|
const out = [];
|
|
128
339
|
for (const h of helpers) {
|
|
340
|
+
const family = helperFamilyOf(h, fallback);
|
|
129
341
|
if (family.routeDuplicates && h.method && h.normalizedPath) {
|
|
130
342
|
const key = `${h.method} ${h.normalizedPath}`;
|
|
131
343
|
const first = byRoute.get(key);
|
|
@@ -143,7 +355,7 @@ function violationsIn(helpers, file, family, byRoute) {
|
|
|
143
355
|
}
|
|
144
356
|
}
|
|
145
357
|
for (const a of h.assertions) {
|
|
146
|
-
if (!a.
|
|
358
|
+
if (!family.isAllowed(spec, a.text)) {
|
|
147
359
|
out.push({
|
|
148
360
|
kind: "body-assertion",
|
|
149
361
|
file,
|