ci(#1401): add no-adhoc-markdown-parsing rule (fence-strip + section-collect) + grandfather burn-down (#1402)
Tightens the over-broad heading-walk detection: removes heading-walk entirely and narrows fence-regex to require a multiline body ([\s\S]), so single-line tests like /^```/ and /^###\s+/ are no longer flagged. Grandfathers the 10 genuine section-collect sites across state.cts, milestone.cts, audit.cts, and phase-lifecycle.cts with concrete reasons. Adds 12 RuleTester tests (3 positive, 9 negative) to eslint-rules.test.cjs. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
131
eslint-rules/no-adhoc-markdown-parsing.cjs
Normal file
131
eslint-rules/no-adhoc-markdown-parsing.cjs
Normal file
@@ -0,0 +1,131 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* no-adhoc-markdown-parsing
|
||||
*
|
||||
* Flags hand-rolled markdown-structure scanning in src/*.cts that duplicates
|
||||
* the canonical seam (src/markdown-sectionizer.cts). Applies to two patterns:
|
||||
*
|
||||
* 1. FENCE-BLOCK-STRIP — regex literals whose source contains a triple-backtick
|
||||
* or triple-tilde fence delimiter AND a multiline body
|
||||
* ([\s\S] or [\S\s]), indicating the regex strips/matches
|
||||
* a fenced CODE BLOCK spanning multiple lines.
|
||||
*
|
||||
* A bare single-line fence-opener test like /^```/ or
|
||||
* /^\s*(?:```|~~~)/ is NOT flagged — that is line
|
||||
* detection / normalisation, not block-stripping.
|
||||
*
|
||||
* 2. SECTION-COLLECT — regex literals of the shape
|
||||
* /(#{...}\n)([\s\S]*?)(?=\n#{...}|$)/ (a heading
|
||||
* capture followed by a non-greedy body up to a heading
|
||||
* lookahead). These hand-roll what collectSection() owns.
|
||||
* Fingerprint: [\\s\\S] (multiline body) AND (?= lookahead
|
||||
* that references a heading anchor #.
|
||||
*
|
||||
* Per-finding exemption: add // allow-adhoc-markdown: <reason> as a
|
||||
* trailing comment on the same source line, OR as a standalone comment on the
|
||||
* line immediately preceding the flagged node. (Mirrors no-source-grep's
|
||||
* // allow-test-rule: mechanism but is scoped to individual findings.)
|
||||
*
|
||||
* Authors must import from src/markdown-sectionizer.cts instead.
|
||||
*/
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const rule = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'Disallow hand-rolled markdown-structure scanning (fence-block-strip, section-collect) in src/*.cts — import the markdown-sectionizer seam instead.',
|
||||
category: 'Best Practices',
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
fenceRegex:
|
||||
'Ad-hoc fence-block-strip regex detected (triple-fence delimiter + multiline body). Import stripFencedCode() from ./markdown-sectionizer instead. Suppress with: // allow-adhoc-markdown: <reason>',
|
||||
sectionCollect:
|
||||
'Ad-hoc section-collect regex detected (heading + [\\s\\S]*? + lookahead). Import collectSection() from ./markdown-sectionizer instead. Suppress with: // allow-adhoc-markdown: <reason>',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
// Only run on src/*.cts files
|
||||
const filename = context.getFilename ? context.getFilename() : context.filename;
|
||||
if (!/(?:^|\/)src\/[^/]+\.cts$/.test(filename.replace(/\\/g, '/'))) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const sourceCode = context.getSourceCode ? context.getSourceCode() : context.sourceCode;
|
||||
|
||||
/**
|
||||
* Check whether a node has a trailing // allow-adhoc-markdown: <reason>
|
||||
* comment on the same source line, OR a standalone allow comment on the
|
||||
* line immediately before the node's start line.
|
||||
*/
|
||||
function isAllowed(node) {
|
||||
const nodeStartLine = node.loc.start.line;
|
||||
|
||||
const allComments = sourceCode.getAllComments();
|
||||
return allComments.some((c) => {
|
||||
if (!/allow-adhoc-markdown:\s*\S/.test(c.value)) return false;
|
||||
// Same line, or one line above
|
||||
return c.loc.start.line === nodeStartLine || c.loc.start.line === nodeStartLine - 1;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Fence-block-strip detection ──────────────────────────────────────────
|
||||
// A regex literal whose source contains ``` or ~~~ AND contains [\s\S] or
|
||||
// [\S\s] (a multiline body), indicating it strips/matches a fenced block.
|
||||
// A bare /^```/ or /^\s*(?:```|~~~)/ (line-detection, no multiline body)
|
||||
// is explicitly NOT flagged.
|
||||
const TRIPLE_BACKTICK = '```'; // ```
|
||||
const TRIPLE_TILDE = '~~~';
|
||||
|
||||
function isFenceBlockStripRegex(node) {
|
||||
if (node.type !== 'Literal' || !node.regex) return false;
|
||||
const src = node.regex.pattern || '';
|
||||
// Must contain a triple fence delimiter
|
||||
if (!src.includes(TRIPLE_BACKTICK) && !src.includes(TRIPLE_TILDE)) return false;
|
||||
// Must ALSO contain a multiline body marker — i.e. it spans blocks, not just lines
|
||||
const hasMultilineBody = src.includes('[\\s\\S]') || src.includes('[\\S\\s]');
|
||||
return hasMultilineBody;
|
||||
}
|
||||
|
||||
// ── Section-collect regex detection ─────────────────────────────────────
|
||||
// Matches patterns of the shape:
|
||||
// /(#{1,6}...\n)([\s\S]*?)(?=\n#{...}|$)/
|
||||
// The key fingerprint is: [\\s\\S] (or [\s\S]) AND (?= (lookahead) AND # in
|
||||
// the same regex, forming the "body up to next heading" construct.
|
||||
function isSectionCollectRegex(node) {
|
||||
if (node.type !== 'Literal' || !node.regex) return false;
|
||||
const src = node.regex.pattern || '';
|
||||
// Must contain [\s\S] (the non-greedy body)
|
||||
const hasMultilineBody = src.includes('[\\s\\S]') || src.includes('[\\S\\s]');
|
||||
if (!hasMultilineBody) return false;
|
||||
// Must contain a lookahead (?= that references a heading anchor #
|
||||
const hasHeadingLookahead = /\(\?=.*#/.test(src);
|
||||
return hasHeadingLookahead;
|
||||
}
|
||||
|
||||
return {
|
||||
Literal(node) {
|
||||
// 1. Fence-block-strip regex
|
||||
if (isFenceBlockStripRegex(node)) {
|
||||
if (!isAllowed(node)) {
|
||||
context.report({ node, messageId: 'fenceRegex' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Section-collect regex
|
||||
if (isSectionCollectRegex(node)) {
|
||||
if (!isAllowed(node)) {
|
||||
context.report({ node, messageId: 'sectionCollect' });
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = rule;
|
||||
@@ -14,6 +14,7 @@ import noMagicSleepInTests from './eslint-rules/no-magic-sleep-in-tests.cjs';
|
||||
import noElapsedAssertion from './eslint-rules/no-elapsed-assertion.cjs';
|
||||
import noRawRmsyncInTests from './eslint-rules/no-raw-rmsync-in-tests.cjs';
|
||||
import noTautologicalAssert from './eslint-rules/no-tautological-assert.cjs';
|
||||
import noAdhocMarkdownParsing from './eslint-rules/no-adhoc-markdown-parsing.cjs';
|
||||
|
||||
const localPlugin = {
|
||||
rules: {
|
||||
@@ -22,6 +23,7 @@ const localPlugin = {
|
||||
'no-elapsed-assertion': noElapsedAssertion,
|
||||
'no-raw-rmsync-in-tests': noRawRmsyncInTests,
|
||||
'no-tautological-assert': noTautologicalAssert,
|
||||
'no-adhoc-markdown-parsing': noAdhocMarkdownParsing,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -171,6 +173,9 @@ export default tseslint.config(
|
||||
// these rules add lint-level coverage. warn-first per the harness convention.
|
||||
{
|
||||
files: ['src/**/*.cts'],
|
||||
plugins: {
|
||||
local: localPlugin,
|
||||
},
|
||||
extends: [tseslint.configs.recommendedTypeChecked],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
@@ -180,6 +185,9 @@ export default tseslint.config(
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
|
||||
// ADR-1372 T7: enforce use of the markdown-sectionizer seam; grandfather
|
||||
// pre-migration sites with // allow-adhoc-markdown: <reason>
|
||||
'local/no-adhoc-markdown-parsing': 'error',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ function scanDebugSessions(planDir: string): DebugSessionItem[] {
|
||||
|
||||
// Extract hypothesis from "Current Focus" block if parseable
|
||||
let hypothesis = '';
|
||||
const focusMatch = content.match(/##\s*Current Focus[^\n]*\n([\s\S]*?)(?=\n##\s|$)/i);
|
||||
const focusMatch = content.match(/##\s*Current Focus[^\n]*\n([\s\S]*?)(?=\n##\s|$)/i); // allow-adhoc-markdown: pre-seam read-only section extract in audit.cts; pending migration #1372
|
||||
if (focusMatch) {
|
||||
const focusText = focusMatch[1].trim().split('\n')[0].trim();
|
||||
hypothesis = sanitizeForDisplay(focusText.slice(0, 100));
|
||||
@@ -649,7 +649,7 @@ function scanContextQuestions(planDir: string): ContextQuestionItem[] {
|
||||
|
||||
// Also check for ## Open Questions section in body
|
||||
if (questions.length === 0) {
|
||||
const oqMatch = content.match(/##\s*Open Questions[^\n]*\n([\s\S]*?)(?=\n##\s|$)/i);
|
||||
const oqMatch = content.match(/##\s*Open Questions[^\n]*\n([\s\S]*?)(?=\n##\s|$)/i); // allow-adhoc-markdown: pre-seam read-only section extract in audit.cts; pending migration #1372
|
||||
if (oqMatch) {
|
||||
const oqBody = oqMatch[1].trim();
|
||||
if (oqBody && oqBody.length > 0 && !/^\s*none\s*$/i.test(oqBody)) {
|
||||
|
||||
@@ -319,7 +319,7 @@ function cmdMilestoneComplete(cwd: string, version: string, options: MilestoneCo
|
||||
|
||||
// Reset Current Position narrative so resume/progress flows do not keep
|
||||
// pointing at closed-phase execution instructions.
|
||||
const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
|
||||
const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i; // allow-adhoc-markdown: pre-seam section write-modify in milestone.cts; pending collectSection migration #1372
|
||||
const closedPositionBody =
|
||||
`\nPhase: Milestone ${version} complete\n` +
|
||||
`Plan: —\n` +
|
||||
@@ -332,7 +332,7 @@ function cmdMilestoneComplete(cwd: string, version: string, options: MilestoneCo
|
||||
}
|
||||
|
||||
// Normalize operator-next-step tails that can become stale after close.
|
||||
const operatorPattern = /(##\s*Operator Next Steps\s*\n)([\s\S]*?)(?=\n##|$)/i;
|
||||
const operatorPattern = /(##\s*Operator Next Steps\s*\n)([\s\S]*?)(?=\n##|$)/i; // allow-adhoc-markdown: pre-seam section write-modify in milestone.cts; pending collectSection migration #1372
|
||||
if (operatorPattern.test(stateContent)) {
|
||||
stateContent = stateContent.replace(
|
||||
operatorPattern,
|
||||
|
||||
@@ -49,6 +49,7 @@ export function deriveProgressFromRoadmap(roadmapContent: string): RoadmapProgre
|
||||
// Count total phase rows in the progress table.
|
||||
// Identify the table by looking for Phase|...|Status|...|Completed header.
|
||||
const progressTableMatch = roadmapContent.match(
|
||||
// allow-adhoc-markdown: table-scoped regex with heading lookahead as stop; table parsing, out of seam scope; pending #1372
|
||||
/\|\s*Phase\s*\|[^|]*\|[^|]*Status[^|]*\|[^|]*Completed[^|]*\|[\s\S]*?(?=\n\n|\n##|$)/i,
|
||||
);
|
||||
if (progressTableMatch) {
|
||||
|
||||
@@ -568,7 +568,7 @@ function cmdStateRecordMetric(cwd: string, options: StateRecordMetricOptions, ra
|
||||
let created = false;
|
||||
readModifyWriteStateMd(statePath, (content) => {
|
||||
// Find Performance Metrics section and its table
|
||||
const metricsPattern = /(##\s*Performance Metrics[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n)([\s\S]*?)(?=\n##|\n$|$)/i;
|
||||
const metricsPattern = /(##\s*Performance Metrics[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n)([\s\S]*?)(?=\n##|\n$|$)/i; // allow-adhoc-markdown: metrics-table write-path section-collect in state.cts; pending collectSection migration #1372
|
||||
const metricsMatch = content.match(metricsPattern);
|
||||
|
||||
const newRow = `| Phase ${phase} P${plan} | ${duration} | ${tasks || '-'} tasks | ${files || '-'} files |`;
|
||||
@@ -1138,8 +1138,8 @@ function cmdStateRecordSession(cwd: string, options: StateRecordSessionOptions,
|
||||
* Returns the match whose group 1 is the section body, or null.
|
||||
*/
|
||||
function matchSessionSection(body: string): RegExpMatchArray | null {
|
||||
return body.match(/(?:^|\n)##[ \t]*Session[ \t]*\n([\s\S]*?)(?=\n##|$)/i)
|
||||
|| body.match(/(?:^|\n)##[ \t]*Session Continuity[ \t]*\n([\s\S]*?)(?=\n##|$)/i);
|
||||
return body.match(/(?:^|\n)##[ \t]*Session[ \t]*\n([\s\S]*?)(?=\n##|$)/i) // allow-adhoc-markdown: read-only session-section extract in state.cts; pending collectSection migration #1372
|
||||
|| body.match(/(?:^|\n)##[ \t]*Session Continuity[ \t]*\n([\s\S]*?)(?=\n##|$)/i); // allow-adhoc-markdown: read-only session-continuity section extract in state.cts; pending collectSection migration #1372
|
||||
}
|
||||
|
||||
function parseProsePhaseField(value: string | null): { phase: string | null; name: string | null } {
|
||||
@@ -1218,7 +1218,7 @@ function cmdStateSnapshot(cwd: string, raw: boolean): void {
|
||||
|
||||
// Extract decisions table
|
||||
const decisions: Array<{ phase: string; summary: string; rationale: string }> = [];
|
||||
const decisionsMatch = body.match(/##\s*Decisions Made[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n([\s\S]*?)(?=\n##|\n$|$)/i);
|
||||
const decisionsMatch = body.match(/##\s*Decisions Made[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n([\s\S]*?)(?=\n##|\n$|$)/i); // allow-adhoc-markdown: read-only decisions-table section-collect in state.cts; pending collectSection migration #1372
|
||||
if (decisionsMatch) {
|
||||
const tableBody = decisionsMatch[1];
|
||||
const rows = tableBody.trim().split('\n').filter(r => r.includes('|'));
|
||||
@@ -1236,7 +1236,7 @@ function cmdStateSnapshot(cwd: string, raw: boolean): void {
|
||||
|
||||
// Extract blockers list
|
||||
const blockers: string[] = [];
|
||||
const blockersMatch = body.match(/##\s*Blockers\s*\n([\s\S]*?)(?=\n##|$)/i);
|
||||
const blockersMatch = body.match(/##\s*Blockers\s*\n([\s\S]*?)(?=\n##|$)/i); // allow-adhoc-markdown: read-only blockers section-collect in state.cts; pending collectSection migration #1372
|
||||
if (blockersMatch) {
|
||||
const blockersSection = blockersMatch[1];
|
||||
const items = blockersSection.match(/^-\s+(.+)$/gm) || [];
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
* - local/no-magic-sleep-in-tests
|
||||
* - local/no-elapsed-assertion
|
||||
* - local/no-raw-rmsync-in-tests
|
||||
* - local/no-adhoc-markdown-parsing
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
@@ -19,6 +20,7 @@ const noMagicSleepInTests = require('../eslint-rules/no-magic-sleep-in-tests.cjs
|
||||
const noElapsedAssertion = require('../eslint-rules/no-elapsed-assertion.cjs');
|
||||
const noRawRmsyncInTests = require('../eslint-rules/no-raw-rmsync-in-tests.cjs');
|
||||
const noTautologicalAssert = require('../eslint-rules/no-tautological-assert.cjs');
|
||||
const noAdhocMarkdownParsing = require('../eslint-rules/no-adhoc-markdown-parsing.cjs');
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: {
|
||||
@@ -812,3 +814,167 @@ describe('no-tautological-assert rule', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── no-adhoc-markdown-parsing ───────────────────────────────────────────────
|
||||
|
||||
describe('no-adhoc-markdown-parsing rule', () => {
|
||||
test('rule module exports a create function', () => {
|
||||
assert.strictEqual(typeof noAdhocMarkdownParsing.create, 'function');
|
||||
});
|
||||
|
||||
// ── POSITIVE cases: flag fence-block-strip and section-collect ────────────
|
||||
|
||||
test('invalid: fence-block-strip regex with triple-backtick and multiline body', () => {
|
||||
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
// /```[\s\S]*?```/ — triple-backtick + [\s\S] body → flagged as fenceRegex
|
||||
code: String.raw`const stripFences = /` + '```' + String.raw`[\s\S]*?` + '```' + '/;',
|
||||
filename: 'src/some-module.cts',
|
||||
errors: [{ messageId: 'fenceRegex' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('invalid: fence-block-strip regex with triple-tilde and multiline body', () => {
|
||||
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
// /~~~[\s\S]*?~~~/ — triple-tilde + [\s\S] body → flagged as fenceRegex
|
||||
code: String.raw`const stripTildes = /~~~[\s\S]*?~~~/;`,
|
||||
filename: 'src/some-module.cts',
|
||||
errors: [{ messageId: 'fenceRegex' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('invalid: section-collect regex with heading capture, multiline body, heading lookahead', () => {
|
||||
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
// /(##\s*X\n)([\s\S]*?)(?=\n##|$)/ — the classic section-collect fingerprint
|
||||
code: String.raw`const pat = /(##\s*X\n)([\s\S]*?)(?=\n##|$)/;`,
|
||||
filename: 'src/some-module.cts',
|
||||
errors: [{ messageId: 'sectionCollect' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
// ── NEGATIVE cases: single-line fence tests and heading matches NOT flagged ─
|
||||
|
||||
test('valid: bare single-line fence-opener /^```/ is NOT flagged', () => {
|
||||
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
|
||||
valid: [
|
||||
{
|
||||
code: 'const fenceRegex = /^' + '```' + '/;',
|
||||
filename: 'src/some-module.cts',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('valid: /^\\s*(?:```|~~~)/ fence-line test is NOT flagged', () => {
|
||||
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
|
||||
valid: [
|
||||
{
|
||||
code: String.raw`const isFenceLine = /^\s*(?:` + '```' + String.raw`|~~~)/;`,
|
||||
filename: 'src/some-module.cts',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('valid: /^#\\s+/ single-line title-find is NOT flagged', () => {
|
||||
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
|
||||
valid: [
|
||||
{
|
||||
code: String.raw`const titleRe = /^#\s+/;`,
|
||||
filename: 'src/some-module.cts',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('valid: /^###\\s+(.+?)\\s*$/ single-line heading-category match is NOT flagged', () => {
|
||||
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
|
||||
valid: [
|
||||
{
|
||||
code: String.raw`const headingRe = /^###\s+(.+?)\s*$/;`,
|
||||
filename: 'src/some-module.cts',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('valid: /^(#{1,6})\\s+(.*)/ single-line heading match is NOT flagged', () => {
|
||||
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
|
||||
valid: [
|
||||
{
|
||||
code: String.raw`const headingM = line.match(/^(#{1,6})\s+(.*)/);`,
|
||||
filename: 'src/some-module.cts',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('valid: seam usage (no regex, just an import reference) is NOT flagged', () => {
|
||||
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
const { collectSection } = require('./markdown-sectionizer');
|
||||
const result = collectSection(content, 'Introduction');
|
||||
`,
|
||||
filename: 'src/some-module.cts',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('valid: annotated fence-block-strip with allow-adhoc-markdown is NOT flagged', () => {
|
||||
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
|
||||
valid: [
|
||||
{
|
||||
// Trailing annotation on the same line suppresses the finding
|
||||
code:
|
||||
'const stripFences = /```' +
|
||||
String.raw`[\s\S]*?` +
|
||||
'`' +
|
||||
'``/; // allow-adhoc-markdown: pre-seam write path; pending migration #1372',
|
||||
filename: 'src/some-module.cts',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('valid: rule is inert outside src/*.cts files', () => {
|
||||
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
|
||||
valid: [
|
||||
{
|
||||
// Same fence-block-strip regex in a test file → rule does not apply
|
||||
code: String.raw`const stripFences = /~~~[\s\S]*?~~~/;`,
|
||||
filename: 'tests/some.test.cjs',
|
||||
},
|
||||
{
|
||||
// Same regex in a scripts file → rule does not apply
|
||||
code: String.raw`const p = /(##\s*X\n)([\s\S]*?)(?=\n##|$)/;`,
|
||||
filename: 'scripts/helper.cjs',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user