feat(plan): the Test first File line follows the 1.9 grammar [skip-docs]
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3e1a811c60
commit
fc157c3ad5
3 changed files with 132 additions and 9 deletions
|
|
@ -578,15 +578,41 @@ export function stepTestsOnly(body, version) {
|
|||
|
||||
// A line of the `Test first:` field that names its test files (6.3.1's selector).
|
||||
const TEST_FIRST_FILE_LINE = /\bFile:\**[ \t]*`/i;
|
||||
// From plan_version 1.9: a line that opens with `File:` (after an optional
|
||||
// list marker and `**`), backticked or not.
|
||||
const TEST_FIRST_FILE_LINE_19 = /^\s*(?:[-*+]\s+)?(?:\*\*)?File:(?:\*\*)?/i;
|
||||
|
||||
/**
|
||||
* The step's test files: every file on the `Test first:` `File:` line. A line
|
||||
* counts as that line exactly when TEST_FIRST_FILE_LINE matches it. Below
|
||||
* plan_version 1.9 (or without a version) every backticked span on the line,
|
||||
* from the match on, that has the shape of a path is a test file (a call
|
||||
* `parse()`, an option, a URL is not), normalised and deduplicated. A
|
||||
* `Verifies:` line without a `File:` is never read. Every version reads this
|
||||
* way until the 1.9 branch exists.
|
||||
* plan_version 1.9: the rest of each `Test first:` `File:` line (after `File:`
|
||||
* and any `**`), one entry per line, in order. The one selector both
|
||||
* testFirstFiles and the plan validator's grammar check read.
|
||||
* @param {Map<string, {text: string}>} fields - parseStepFields(body)
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function testFirstFileEntries(fields) {
|
||||
if (!fields || !fields.has('Test first')) return [];
|
||||
const out = [];
|
||||
for (const line of String(fields.get('Test first').text ?? '').split('\n')) {
|
||||
const at = TEST_FIRST_FILE_LINE_19.exec(line);
|
||||
if (at) out.push(line.slice(at[0].length).trim());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* The step's test files: every file on the `Test first:` `File:` line.
|
||||
*
|
||||
* Below plan_version 1.9 (or without a version) a line counts as that line
|
||||
* exactly when TEST_FIRST_FILE_LINE matches it, and every backticked span on
|
||||
* the line, from the match on, that has the shape of a path is a test file (a
|
||||
* call `parse()`, an option, a URL is not).
|
||||
*
|
||||
* From 1.9 a line counts when it opens with `File:` (testFirstFileEntries),
|
||||
* backticked or not, and its rest is one entry read by entryPaths: the
|
||||
* leading backticked paths are the test files, the rest is description.
|
||||
*
|
||||
* Normalised, in order, deduplicated. A `Verifies:` line without a `File:` is
|
||||
* never read.
|
||||
* @param {Map<string, {text: string}>} fields - parseStepFields(body)
|
||||
* @param {string|null} [version] - the plan's plan_version
|
||||
* @returns {string[]}
|
||||
|
|
@ -594,6 +620,12 @@ const TEST_FIRST_FILE_LINE = /\bFile:\**[ \t]*`/i;
|
|||
export function testFirstFiles(fields, version) {
|
||||
if (!fields || !fields.has('Test first')) return [];
|
||||
const out = [];
|
||||
if (versionAtLeast(version, '1.9')) {
|
||||
for (const entry of testFirstFileEntries(fields)) {
|
||||
for (const p of entryPaths(entry).files) if (!out.includes(p)) out.push(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
for (const line of String(fields.get('Test first').text ?? '').split('\n')) {
|
||||
const at = TEST_FIRST_FILE_LINE.exec(line);
|
||||
if (!at) continue;
|
||||
|
|
|
|||
|
|
@ -42,7 +42,9 @@
|
|||
// anything else, `none`, and an empty or absent `Files:` are
|
||||
// PLAN_STEP_PATH_NOT_FIRST; a leading span that is no path (`parse()`, an
|
||||
// option, a URL) is PLAN_STEP_PATH_NOT_A_PATH. Errors under strict,
|
||||
// warnings under --soft (checkPathGrammar).
|
||||
// warnings under --soft (checkPathGrammar). Each `Test first:` `File:`
|
||||
// line (a line that opens with `File:`, backticked or not) is one entry
|
||||
// under the same grammar and raises the same two codes.
|
||||
// - below 1.9, a step whose `Files:` reader (plan-schema `fieldPaths`) finds a
|
||||
// path outside backticks gets the warning PLAN_STEP_PATH_UNTICKED, under
|
||||
// --strict too; the plan is read and gated as before.
|
||||
|
|
@ -53,6 +55,7 @@ import {
|
|||
parseStepFields, findSection, splitSections, findModuleCycle, parseMermaid, fencedBlocks,
|
||||
versionAtLeast, PLAN_VERSION_CURRENT, PLAN_VERSIONS_SUPPORTED, CEREMONY_VALUES,
|
||||
ON_FAILURE_ACTIONS, onFailureAction, stepTestsOnlyFromFields, entryPaths, untickedPaths,
|
||||
testFirstFileEntries,
|
||||
} from '../parsers/plan-schema.mjs';
|
||||
import { validateAllManifests } from '../parsers/manifest-yaml.mjs';
|
||||
import { parseDocument } from '../util/frontmatter.mjs';
|
||||
|
|
@ -200,7 +203,8 @@ function checkRecovery(sections, opts, planVersion) {
|
|||
/**
|
||||
* plan_version 1.9: every `Files:` entry opens with its backticked path(s)
|
||||
* (plan-schema `entryPaths`). An absent `Files:` is read as empty text, so it
|
||||
* names no file.
|
||||
* names no file. Each `Test first:` `File:` line (plan-schema
|
||||
* `testFirstFileEntries`) is one entry under the same grammar.
|
||||
*/
|
||||
function checkPathGrammar(sections) {
|
||||
const out = [];
|
||||
|
|
@ -222,6 +226,23 @@ function checkPathGrammar(sections) {
|
|||
'the description follows (`` `src/a.mjs` - the parser ``). A step that changes no file names none.'));
|
||||
}
|
||||
}
|
||||
for (const entry of testFirstFileEntries(f)) {
|
||||
for (const i of entryPaths(entry).issues) {
|
||||
if (i.code === 'PLAN_STEP_PATH_NOT_A_PATH') {
|
||||
out.push(issue('PLAN_STEP_PATH_NOT_A_PATH',
|
||||
`Step ${s.n}: a \`Test first:\` \`File:\` line opens with a backticked span that is no path: ${entry}`,
|
||||
'plan_version 1.9: the leading backticked spans of the `File:` line are the test files; a call ' +
|
||||
'(`parse()`), an option or a URL is description and goes after the path.'));
|
||||
} else {
|
||||
out.push(issue('PLAN_STEP_PATH_NOT_FIRST',
|
||||
entry === ''
|
||||
? `Step ${s.n}: a \`Test first:\` \`File:\` line names no file`
|
||||
: `Step ${s.n}: a \`Test first:\` \`File:\` line does not open with a backticked path: ${entry}`,
|
||||
'plan_version 1.9: the `File:` line opens with its backticked test file(s), separated by commas or ' +
|
||||
'spaces; the description follows (`` File: `tests/a.test.mjs` (new) ``).'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue