Files
msd-core/tests/product-name-purity.test.cjs
Tom Boucher 918f987a19 feat(#2982): extend no-source-grep lint to catch var-binding readFileSync.includes() (#2985)
* feat(#2982): extend no-source-grep lint to catch var-binding readFileSync.includes()

The base lint (scripts/lint-no-source-grep.cjs) only catches
readFileSync(...).<text-method>() chained directly. The much more
common var-binding form escapes it:

  const src = fs.readFileSync(p, 'utf8');
  // 50 lines later
  if (src.includes('foo')) {}        // ← still grep, lint missed it

Scan of the test suite found ~141 files using this pattern.

Implementation built TDD per #2982 with structured-IR assertions:

  scripts/lint-no-source-grep-extras.cjs
    - detectVarBindingViolations(src) — pure detector, two passes:
      pass 1 collects vars bound from readFileSync, pass 2 finds any
      <var>.<includes|startsWith|endsWith|match|search>( on those vars.
    - detectWrappedAssertOkMatch(src) — flags
      assert.ok(<expr>.match(...)) which escapes the assert.match rule.
    - VIOLATION enum exposes stable codes for tests to assert on.

  scripts/lint-no-source-grep.cjs
    - Wires the new detectors into the existing per-file check; one
      additional violation row per file with the first 3 sample tokens.

  tests/bug-2982-lint-var-binding.test.cjs
    - 13 tests, all assertions on typed VIOLATION enum / structured
      records. Covers all 5 text-match methods, multi-var, no-bind,
      string literal (must NOT trigger), wrapped assert.ok(.match),
      and assert.match (must NOT double-flag).

Migration backlog (#2974 expanded scope):

  - 42 files annotated `// allow-test-rule: source-text-is-the-product`
    (legitimate — they read .md/.json/.yml files whose deployed text
    IS the product)
  - 3 files annotated `// allow-test-rule: pending-migration-to-typed-ir [#2974]`
    (read .cjs/.js source — clear migration debt)
  - 95 files annotated `pending-migration-to-typed-ir [#2974]` with
    `Per-file review may reclassify as source-text-is-the-product
    during migration` (mixed — manual review under #2974)

After this lands the lint reports 0 violations on main; new
violations in PRs surface immediately.

Closes #2982
Refs #2974

* test(#2982): fix truncated test name per CR

The label ended with a bare '(' from a copy-paste mishap. Now reads
'does NOT flag .matchAll(...) — matchAll is not match, so
assert.ok(.matchAll(...)) is not flagged'.

* chore(#2982): add changeset fragment for PR #2985

* chore(#2982): add changeset fragment for PR #2985
2026-05-01 19:50:10 -04:00

116 lines
3.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// allow-test-rule: pending-migration-to-typed-ir [#2974]
// Tracked in #2974 for migration to typed-IR assertions per CONTRIBUTING.md
// "Prohibited: Raw Text Matching on Test Outputs". Per-file review may
// reclassify some entries as source-text-is-the-product during migration.
/**
* Regression guard for #1777: product names must not have parenthetical descriptions.
*
* Community PRs repeatedly add editorial commentary in parentheses next to
* product names (licensing, parent company, architecture). This test scans
* all README files and ensures install-block comment lines contain only the
* product name — no parenthetical text of any kind.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..');
// Product names that appear in install blocks as comment headers
const PRODUCTS = [
'Claude Code', 'Claude', 'OpenCode', 'Kilo', 'Codex', 'Copilot',
'Cursor', 'Windsurf', 'Antigravity', 'Trae', 'Cline', 'Augment',
'Gemini', 'Gemini CLI',
];
// README files to scan (root + i18n variants + docs)
const README_FILES = [
'README.md',
'README.ko-KR.md',
'README.ja-JP.md',
'README.zh-CN.md',
'README.pt-BR.md',
'docs/zh-CN/README.md',
'docs/ko-KR/README.md',
'docs/ja-JP/README.md',
'docs/pt-BR/README.md',
'docs/README.md',
].filter(f => fs.existsSync(path.join(ROOT, f)));
describe('product name purity (#1777)', () => {
test('no README install-block comments contain parenthetical descriptions', () => {
const violations = [];
for (const file of README_FILES) {
const content = fs.readFileSync(path.join(ROOT, file), 'utf-8');
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Match shell comment lines that start with # followed by a product name
// and then have parenthetical text: # ProductName (something)
// Also match fullwidth parens used in CJK: # ProductName(something)
const match = line.match(/^#\s+(\S+(?:\s+\S+)?)\s*[((].+[))]/);
if (!match) continue;
const name = match[1];
// Check if this is actually a product name line (not a random comment)
const isProduct = PRODUCTS.some(p =>
name === p || name.startsWith(p)
);
if (isProduct) {
violations.push([
file + ':' + (i + 1),
line.trim(),
].join(' — '));
}
}
}
assert.strictEqual(
violations.length, 0,
[
'Product names in README install blocks must not have parenthetical descriptions.',
'Found violations:',
...violations.map(v => ' ' + v),
].join('\n')
);
});
test('CHANGELOG does not include parenthetical product descriptions', () => {
const changelog = path.join(ROOT, 'CHANGELOG.md');
if (!fs.existsSync(changelog)) return;
const content = fs.readFileSync(changelog, 'utf-8');
const violations = [];
for (const product of PRODUCTS) {
// Match "ProductName (something)" but not "ProductName (v1.2.3)" (version refs are ok)
const pattern = new RegExp(
product.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
'\\s*\\([^)]*(?!v?\\d+\\.\\d)[^)]*\\)',
'g'
);
const matches = content.match(pattern);
if (matches) {
for (const m of matches) {
// Skip version references like "Claude Code (v1.32.0)"
if (/\(v?\d+\.\d+/.test(m)) continue;
violations.push(m);
}
}
}
assert.strictEqual(
violations.length, 0,
[
'CHANGELOG must not include parenthetical product descriptions.',
'Found:',
...violations.map(v => ' ' + v),
].join('\n')
);
});
});