diff --git a/.github/workflows/generate.yml b/.github/workflows/generate.yml index e19b55a0..bf0bddc0 100644 --- a/.github/workflows/generate.yml +++ b/.github/workflows/generate.yml @@ -79,8 +79,7 @@ jobs: -t ${{ matrix.target }} \ -i "${{ matrix.input }}" \ -o "out/${{ matrix.target }}" \ - --index ./node/doc/api/index.md \ - --skip-lint + --index ./node/doc/api/index.md - name: Upload ${{ matrix.target }} artifacts uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 diff --git a/README.md b/README.md index ff9083ca..47676369 100644 --- a/README.md +++ b/README.md @@ -38,16 +38,14 @@ $ npx doc-kit --help ``` Usage: @nodejs/doc-kit [options] [command] -CLI tool to generate and lint Node.js API documentation +CLI tool to generate the Node.js API documentation Options: -h, --help display help for command Commands: generate [options] Generate API docs - lint [options] Run linter independently interactive Launch guided CLI wizard - list List the given type help [command] display help for command ``` @@ -67,23 +65,6 @@ Options: -c, --changelog Changelog URL or path (default: "https://raw.githubusercontent.com/nodejs/node/HEAD/CHANGELOG.md") --git-ref Git ref/commit URL (default: "https://github.com/nodejs/node/tree/HEAD") -t, --target [modes...] Target generator modes (choices: "json-simple", "legacy-html", "legacy-html-all", "man-page", "legacy-json", "legacy-json-all", "addon-verify", "api-links", "orama-db", "llms-txt") - --no-lint Skip lint before generate - -h, --help display help for command -``` - -### `lint` - -``` -Usage: @nodejs/doc-kit lint [options] - -Run linter independently - -Options: - -i, --input Input file patterns (glob) - --ignore [patterns...] Ignore patterns (comma-separated) - --disable-rule [rules...] Disable linter rules (choices: "duplicate-stability-nodes", "invalid-change-version", "missing-introduced-in") - --dry-run Dry run mode (default: false) - -r, --reporter Linter reporter to use -h, --help display help for command ``` @@ -97,17 +78,3 @@ Launch guided CLI wizard Options: -h, --help display help for command ``` - -### `list` - -``` -Usage: @nodejs/doc-kit list [options] - -List the given type - -Arguments: - types The type to list (choices: "generators", "rules", "reporters") - -Options: - -h, --help display help for command -``` diff --git a/bin/cli.mjs b/bin/cli.mjs index fcf8a7ec..05533cde 100755 --- a/bin/cli.mjs +++ b/bin/cli.mjs @@ -2,18 +2,17 @@ import process from 'node:process'; -import { Argument, Command, Option } from 'commander'; +import { Command, Option } from 'commander'; import commands from './commands/index.mjs'; import interactive from './commands/interactive.mjs'; -import list, { types } from './commands/list.mjs'; import { errorWrap } from './utils.mjs'; const program = new Command() .name('@nodejs/doc-kit') - .description('CLI tool to generate and lint Node.js API documentation'); + .description('CLI tool to generate the Node.js API documentation'); -// Registering generate and lint commands +// Registering commands commands.forEach(({ name, description, options, action }) => { const cmd = program.command(name).description(description); @@ -44,12 +43,5 @@ program .description('Launch guided CLI wizard') .action(errorWrap(interactive)); -// Register the list command -program - .command('list') - .addArgument(new Argument('', 'The type to list').choices(types)) - .description('List the given type') - .action(errorWrap(list)); - // Parse and execute command-line arguments program.parse(process.argv); diff --git a/bin/commands/generate.mjs b/bin/commands/generate.mjs index f13fb150..12bfe8d4 100644 --- a/bin/commands/generate.mjs +++ b/bin/commands/generate.mjs @@ -9,8 +9,6 @@ import { } from '../../src/constants.mjs'; import { publicGenerators } from '../../src/generators/index.mjs'; import createGenerator from '../../src/generators.mjs'; -import createLinter from '../../src/linter/index.mjs'; -import { getEnabledRules } from '../../src/linter/utils/rules.mjs'; import { parseChangelog, parseIndex } from '../../src/parsers/markdown.mjs'; import { loadAndParse } from '../utils.mjs'; @@ -25,7 +23,6 @@ const availableGenerators = Object.keys(publicGenerators); * @property {string} changelog - Specifies the path to the Node.js CHANGELOG.md file. * @property {string} [gitRef] - Git ref/commit URL. * @property {number} [threads] - Number of threads to allow. - * @property {boolean} [skipLint] - Skip lint before generate. */ /** @@ -115,15 +112,6 @@ export default { type: 'text', }, }, - skipLint: { - flags: ['--skip-lint'], - desc: 'Skip lint before generate', - prompt: { - type: 'confirm', - message: 'Skip lint before generate?', - initialValue: false, - }, - }, }, /** * Handles the action for generating API docs @@ -131,18 +119,7 @@ export default { * @returns {Promise} */ async action(opts) { - const rules = getEnabledRules(opts.disableRule); - const linter = opts.skipLint ? undefined : createLinter(rules); - - const docs = await loadAndParse(opts.input, opts.ignore, linter); - - linter?.report(); - - if (linter?.hasError()) { - console.error('Lint failed; aborting generation.'); - process.exit(1); - } - + const docs = await loadAndParse(opts.input, opts.ignore); const releases = await parseChangelog(opts.changelog); const index = opts.index && (await parseIndex(opts.index)); diff --git a/bin/commands/index.mjs b/bin/commands/index.mjs index 05a10c93..3e6d9d97 100644 --- a/bin/commands/index.mjs +++ b/bin/commands/index.mjs @@ -1,4 +1,3 @@ import generate from './generate.mjs'; -import lint from './lint.mjs'; -export default [generate, lint]; +export default [generate]; diff --git a/bin/commands/lint.mjs b/bin/commands/lint.mjs deleted file mode 100644 index 08298d81..00000000 --- a/bin/commands/lint.mjs +++ /dev/null @@ -1,96 +0,0 @@ -import process from 'node:process'; - -import createLinter from '../../src/linter/index.mjs'; -import reporters from '../../src/linter/reporters/index.mjs'; -import rules from '../../src/linter/rules/index.mjs'; -import { getEnabledRules } from '../../src/linter/utils/rules.mjs'; -import { loadAndParse } from '../utils.mjs'; - -const availableRules = Object.keys(rules); -const availableReporters = Object.keys(reporters); - -/** - * @typedef {Object} LinterOptions - * @property {Array|string} input - Glob/path for input files. - * @property {Array|string} [ignore] - Glob/path for ignoring files. - * @property {string[]} [disableRule] - Linter rules to disable. - * @property {boolean} [dryRun] - Dry-run mode. - * @property {keyof reporters} reporter - Reporter for linter output. - */ - -/** - * @type {import('../utils.mjs').Command} - */ -export default { - name: 'lint', - description: 'Run linter independently', - options: { - input: { - flags: ['-i', '--input '], - desc: 'Input file patterns (glob)', - prompt: { - type: 'text', - message: 'Enter input glob patterns', - variadic: true, - required: true, - }, - }, - ignore: { - flags: ['--ignore [patterns...]'], - desc: 'Ignore patterns (comma-separated)', - prompt: { - type: 'text', - message: 'Enter ignore patterns', - variadic: true, - }, - }, - disableRule: { - flags: ['--disable-rule [rules...]'], - desc: 'Disable linter rules', - prompt: { - type: 'multiselect', - message: 'Choose rules to disable', - options: availableRules.map(r => ({ label: r, value: r })), - }, - }, - dryRun: { - flags: ['--dry-run'], - desc: 'Dry run mode', - prompt: { - type: 'confirm', - message: 'Enable dry run mode?', - initialValue: false, - }, - }, - reporter: { - flags: ['-r', '--reporter '], - desc: 'Linter reporter to use', - prompt: { - type: 'select', - message: 'Choose a reporter', - options: availableReporters.map(r => ({ label: r, value: r })), - }, - }, - }, - - /** - * Action for running the linter - * @param {LinterOptions} opts - Linter options. - * @returns {Promise} - */ - async action(opts) { - try { - const rules = getEnabledRules(opts.disableRule); - const linter = createLinter(rules, opts.dryRun); - - await loadAndParse(opts.input, opts.ignore, linter); - - linter.report(); - - process.exitCode = +linter.hasError(); - } catch (error) { - console.error('Error running the linter:', error); - process.exitCode = 1; - } - }, -}; diff --git a/bin/commands/list.mjs b/bin/commands/list.mjs deleted file mode 100644 index 50bfb810..00000000 --- a/bin/commands/list.mjs +++ /dev/null @@ -1,27 +0,0 @@ -import { publicGenerators } from '../../src/generators/index.mjs'; -import reporters from '../../src/linter/reporters/index.mjs'; -import rules from '../../src/linter/rules/index.mjs'; - -const availableRules = Object.keys(rules); -const availableReporters = Object.keys(reporters); - -export const types = ['generators', 'rules', 'reporters']; - -/** - * Lists available generators, rules, or reporters based on the given type. - * - * @param {'generators' | 'rules' | 'reporters'} type - The type of items to list. - */ -export default function list(type) { - const list = - type === 'generators' - ? Object.entries(publicGenerators).map( - ([key, generator]) => - `${generator.name || key} (v${generator.version}) - ${generator.description}` - ) - : type === 'rules' - ? availableRules - : availableReporters; - - console.log(list.join('\n')); -} diff --git a/bin/utils.mjs b/bin/utils.mjs index 4645b035..fdb3e70e 100644 --- a/bin/utils.mjs +++ b/bin/utils.mjs @@ -23,12 +23,11 @@ const parser = lazy(createMarkdownParser); * Load and parse markdown API docs. * @param {string[]} input - Glob patterns for input files. * @param {string[]} [ignore] - Glob patterns to ignore. - * @param {import('../src/linter/types').Linter} [linter] - Linter instance * @returns {Promise>>} */ -export async function loadAndParse(input, ignore, linter) { +export async function loadAndParse(input, ignore) { const files = await loader().loadFiles(input, ignore); - return parser(linter).parseApiDocs(files); + return parser().parseApiDocs(files); } /** @@ -49,7 +48,7 @@ export const errorWrap = }; /** - * Represents a command-line option for the linter CLI. + * Represents a command-line option for the CLI. * @typedef {Object} Option * @property {string[]} flags - Command-line flags, e.g., ['-i, --input ']. * @property {string} desc - Description of the option. diff --git a/package-lock.json b/package-lock.json index 3fb427f6..f7f349e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,6 @@ "": { "name": "@nodejs/doc-kit", "dependencies": { - "@actions/core": "^1.11.1", "@clack/prompts": "^0.11.0", "@heroicons/react": "^2.2.0", "@minify-html/node": "^0.16.4", @@ -44,9 +43,7 @@ "shiki": "^3.9.2", "unified": "^11.0.5", "unist-builder": "^4.0.0", - "unist-util-find": "^3.0.0", "unist-util-find-after": "^5.0.0", - "unist-util-find-before": "^4.0.1", "unist-util-position": "^5.0.0", "unist-util-remove": "^4.0.0", "unist-util-select": "^5.1.0", @@ -76,6 +73,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "dev": true, "license": "MIT", "dependencies": { "@actions/exec": "^1.1.1", @@ -86,6 +84,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "dev": true, "license": "MIT", "dependencies": { "@actions/io": "^1.0.1" @@ -95,6 +94,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "dev": true, "license": "MIT", "dependencies": { "tunnel": "^0.0.6", @@ -105,6 +105,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "dev": true, "license": "MIT" }, "node_modules/@alloc/quick-lru": { @@ -470,6 +471,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -6250,12 +6252,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, - "node_modules/lodash.iteratee": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.iteratee/-/lodash.iteratee-4.7.0.tgz", - "integrity": "sha512-yv3cSQZmfpbIKo4Yo45B1taEvxjNvcpF1CEOc0Y6dEyvhPIfEJE3twDwPgWTPQubcSgXyBwBKG6wpQvWMDOf6Q==", - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -9072,6 +9068,7 @@ "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.6.11 <=0.7.0 || >=0.7.3" @@ -9113,6 +9110,7 @@ "version": "5.29.0", "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dev": true, "license": "MIT", "dependencies": { "@fastify/busboy": "^2.0.0" @@ -9160,21 +9158,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-find": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unist-util-find/-/unist-util-find-3.0.0.tgz", - "integrity": "sha512-T7ZqS7immLjYyC4FCp2hDo3ksZ1v+qcbb+e5+iWxc2jONgHOLXPCpms1L8VV4hVxCXgWTxmBHDztuEZFVwC+Gg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "lodash.iteratee": "^4.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/unist-util-find-after": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", @@ -9189,20 +9172,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-find-before": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/unist-util-find-before/-/unist-util-find-before-4.0.1.tgz", - "integrity": "sha512-hfpCNqPbCOseOjHU8oBkRXM8gDQ++Ua3dN7sDYz7VJ+1alt+yd/I+ECZDhv1aqpJ1x47AHbzP/xA0jWf4omAIw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/unist-util-is": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", diff --git a/package.json b/package.json index 89c4c7fc..7d2562bb 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "prettier": "3.6.2" }, "dependencies": { - "@actions/core": "^1.11.1", "@clack/prompts": "^0.11.0", "@heroicons/react": "^2.2.0", "@minify-html/node": "^0.16.4", @@ -76,9 +75,7 @@ "shiki": "^3.9.2", "unified": "^11.0.5", "unist-builder": "^4.0.0", - "unist-util-find": "^3.0.0", "unist-util-find-after": "^5.0.0", - "unist-util-find-before": "^4.0.1", "unist-util-position": "^5.0.0", "unist-util-remove": "^4.0.0", "unist-util-select": "^5.1.0", diff --git a/scripts/vercel-build.sh b/scripts/vercel-build.sh index 6cfa29ff..862d5340 100755 --- a/scripts/vercel-build.sh +++ b/scripts/vercel-build.sh @@ -5,7 +5,6 @@ node bin/cli.mjs generate \ -t web \ -i "./node/doc/api/*.md" \ -o "./out" \ - --index "./node/doc/api/index.md" \ - --skip-lint + --index "./node/doc/api/index.md" rm -rf node/ diff --git a/src/linter/__tests__/fixtures/descriptors.mjs b/src/linter/__tests__/fixtures/descriptors.mjs deleted file mode 100644 index c67a3c75..00000000 --- a/src/linter/__tests__/fixtures/descriptors.mjs +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @type {import('../../types').IssueDescriptor} - */ -export const infoDescriptor = { - level: 'info', - message: 'This is a INFO issue', - position: { start: { line: 1, column: 1 }, end: { line: 1, column: 2 } }, -}; - -/** - * @type {import('../../types').IssueDescriptor} - */ -export const warnDescriptor = { - level: 'warn', - message: 'This is a WARN issue', - position: { start: { line: 1, column: 1 }, end: { line: 1, column: 2 } }, -}; - -/** - * @type {import('../../types').IssueDescriptor} - */ -export const errorDescriptor = { - level: 'error', - message: 'This is a ERROR issue', - position: { start: { line: 1, column: 1 }, end: { line: 1, column: 2 } }, -}; diff --git a/src/linter/__tests__/fixtures/issues.mjs b/src/linter/__tests__/fixtures/issues.mjs deleted file mode 100644 index d3c6f94d..00000000 --- a/src/linter/__tests__/fixtures/issues.mjs +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @type {import('../../types').LintIssue} - */ -export const infoIssue = { - level: 'info', - location: { - path: 'doc/api/test.md', - position: { start: { line: 1, column: 1 }, end: { line: 1, column: 2 } }, - }, - message: 'This is a INFO issue', -}; - -/** - * @type {import('../../types').LintIssue} - */ -export const warnIssue = { - level: 'warn', - location: { - path: 'doc/api/test.md', - position: { start: { line: 1, column: 1 }, end: { line: 1, column: 2 } }, - }, - message: 'This is a WARN issue', -}; - -/** - * @type {import('../../types').LintIssue} - */ -export const errorIssue = { - level: 'error', - location: { - path: 'doc/api/test.md', - position: { start: { line: 1, column: 1 }, end: { line: 1, column: 2 } }, - }, - message: 'This is a ERROR issue', -}; diff --git a/src/linter/__tests__/fixtures/tree.mjs b/src/linter/__tests__/fixtures/tree.mjs deleted file mode 100644 index d1fce2ca..00000000 --- a/src/linter/__tests__/fixtures/tree.mjs +++ /dev/null @@ -1,4 +0,0 @@ -export const emptyTree = { - type: 'root', - children: [], -}; diff --git a/src/linter/__tests__/index.test.mjs b/src/linter/__tests__/index.test.mjs deleted file mode 100644 index 29831275..00000000 --- a/src/linter/__tests__/index.test.mjs +++ /dev/null @@ -1,68 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, mock, it } from 'node:test'; - -import { VFile } from 'vfile'; - -import createContext from '../context.mjs'; -import createLinter from '../index.mjs'; -import { - errorDescriptor, - infoDescriptor, - warnDescriptor, -} from './fixtures/descriptors.mjs'; -import { errorIssue, infoIssue, warnIssue } from './fixtures/issues.mjs'; -import { emptyTree } from './fixtures/tree.mjs'; - -describe('createLinter', () => { - it('should call each rule with context', t => { - const context = { - tree: emptyTree, - report: () => {}, - getIssues: () => [], - }; - - t.mock.fn(createContext).mock.mockImplementation(() => context); - - const rule1 = mock.fn(() => []); - const rule2 = mock.fn(() => []); - - const linter = createLinter([rule1, rule2]); - - linter.lint(new VFile({ path: 'doc/api/test.md' }), emptyTree); - - assert.strictEqual(rule1.mock.callCount(), 1); - assert.strictEqual(rule2.mock.callCount(), 1); - - const rule1Context = rule1.mock.calls[0].arguments[0]; - const rule2Context = rule2.mock.calls[0].arguments[0]; - - assert.strictEqual(rule1Context.tree, emptyTree); - assert.strictEqual(rule2Context.tree, emptyTree); - }); - - it('should return the aggregated issues from all rules', () => { - const rule1 = mock.fn(ctx => { - ctx.report(infoDescriptor); - ctx.report(warnDescriptor); - }); - - const rule2 = mock.fn(ctx => ctx.report(errorDescriptor)); - - const linter = createLinter([rule1, rule2]); - - linter.lint(new VFile({ path: 'doc/api/test.md' }), emptyTree); - - assert.equal(linter.issues.length, 3); - assert.deepEqual(linter.issues, [infoIssue, warnIssue, errorIssue]); - }); - - it('should return an empty array when no issues are found', () => { - const rule = () => []; - - const linter = createLinter([rule]); - - linter.lint(new VFile({ path: 'doc/api/test.md' }), emptyTree); - - assert.deepEqual(linter.issues, []); - }); -}); diff --git a/src/linter/constants.mjs b/src/linter/constants.mjs deleted file mode 100644 index d32fbf8b..00000000 --- a/src/linter/constants.mjs +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -export const INTRODUCED_IN_REGEX = //; - -export const LLM_DESCRIPTION_REGEX = //; - -export const LINT_MESSAGES = { - missingIntroducedIn: "Missing 'introduced_in' field in the API doc entry", - invalidChangeProperty: 'Invalid change property type', - missingChangeVersion: 'Missing version field in the API doc entry', - invalidChangeVersion: 'Invalid version number: {{version}}', - duplicateStabilityNode: 'Duplicate stability node', - missingLlmDescription: - 'Missing llm_description field or paragraph node in the API doc entry', -}; diff --git a/src/linter/context.mjs b/src/linter/context.mjs deleted file mode 100644 index 00a07d8d..00000000 --- a/src/linter/context.mjs +++ /dev/null @@ -1,56 +0,0 @@ -'use strict'; - -/** - * Creates a linting context for a given file and AST tree. - * - * @param {import('vfile').VFile} file - * @param {import('mdast').Root} tree - * @returns {import('./types').LintContext} - */ -const createContext = (file, tree) => { - /** - * Lint issues reported during validation. - * - * @type {import('./types').LintIssue[]} - */ - const issues = []; - - /** - * Reports a lint issue. - * - * @param {import('./types').IssueDescriptor} descriptor - * @returns {void} - */ - const report = ({ level, message, position }) => { - /** - * @type {import('./types').LintIssueLocation} - */ - const location = { - path: file.path, - position, - }; - - issues.push({ - level, - message, - location, - }); - }; - - /** - * Gets all reported issues. - * - * @returns {import('./types').LintIssue[]} - */ - const getIssues = () => { - return issues; - }; - - return { - tree, - report, - getIssues, - }; -}; - -export default createContext; diff --git a/src/linter/index.mjs b/src/linter/index.mjs deleted file mode 100644 index 1f94aa0b..00000000 --- a/src/linter/index.mjs +++ /dev/null @@ -1,74 +0,0 @@ -'use strict'; - -import createContext from './context.mjs'; -import reporters from './reporters/index.mjs'; - -/** - * Creates a linter instance to validate API documentation ASTs against a - * defined set of rules. - * - * @param {import('./types').LintRule[]} rules - Lint rules to apply - * @param {boolean} [dryRun] - If true, the linter runs without reporting - * @returns {import('./types').Linter} - */ -const createLinter = (rules, dryRun = false) => { - /** - * Lint issues collected during validations. - * - * @type {Array} - */ - const issues = []; - - /** - * Lints a API doc and collects issues. - * - * @param {import('vfile').VFile} file - * @param {import('mdast').Root} tree - * @returns {void} - */ - const lint = (file, tree) => { - const context = createContext(file, tree); - - for (const rule of rules) { - rule(context); - } - - issues.push(...context.getIssues()); - }; - - /** - * Reports collected issues using the specified reporter. - * - * @param {keyof typeof reporters} [reporterName] Reporter name - * @returns {void} - */ - const report = (reporterName = 'console') => { - if (dryRun) { - return; - } - - const reporter = reporters[reporterName]; - - for (const issue of issues) { - reporter(issue); - } - }; - - /** - * Checks if any error-level issues were collected. - * - * @returns {boolean} - */ - const hasError = () => { - return issues.some(issue => issue.level === 'error'); - }; - - return { - issues, - lint, - report, - hasError, - }; -}; - -export default createLinter; diff --git a/src/linter/reporters/__tests__/console.test.mjs b/src/linter/reporters/__tests__/console.test.mjs deleted file mode 100644 index 31e3dc4e..00000000 --- a/src/linter/reporters/__tests__/console.test.mjs +++ /dev/null @@ -1,41 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; - -import { - errorIssue, - infoIssue, - warnIssue, -} from '../../__tests__/fixtures/issues.mjs'; -import reporter from '../console.mjs'; - -const testCases = [ - { - issue: infoIssue, - method: 'info', - expected: '\x1B[90mThis is a INFO issue at doc/api/test.md (1:1)\x1B[39m', - }, - { - issue: warnIssue, - method: 'warn', - expected: '\x1B[33mThis is a WARN issue at doc/api/test.md (1:1)\x1B[39m', - }, - { - issue: errorIssue, - method: 'error', - expected: '\x1B[31mThis is a ERROR issue at doc/api/test.md (1:1)\x1B[39m', - }, -]; - -describe('console', () => { - testCases.forEach(({ issue, method, expected }) => { - it(`should use correct colors and output on ${method} issues`, t => { - t.mock.method(console, method); - const mock = console[method].mock; - - reporter(issue); - - assert.equal(mock.callCount(), 1); - assert.deepEqual(mock.calls[0].arguments, [expected]); - }); - }); -}); diff --git a/src/linter/reporters/__tests__/github.test.mjs b/src/linter/reporters/__tests__/github.test.mjs deleted file mode 100644 index 9de9fb68..00000000 --- a/src/linter/reporters/__tests__/github.test.mjs +++ /dev/null @@ -1,25 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; - -import * as issues from '../../__tests__/fixtures/issues.mjs'; -import github from '../github.mjs'; - -describe('github', () => { - it('should write to stdout with the correct fn based on the issue level', t => { - t.mock.method(process.stdout, 'write'); - - Object.values(issues).forEach(github); - - assert.equal(process.stdout.write.mock.callCount(), 3); - - const callsArgs = process.stdout.write.mock.calls.map(call => - call.arguments[0].trim() - ); - - assert.deepEqual(callsArgs, [ - '::error file=doc/api/test.md,line=1,endLine=1::This is a ERROR issue', - '::notice file=doc/api/test.md,line=1,endLine=1::This is a INFO issue', - '::warning file=doc/api/test.md,line=1,endLine=1::This is a WARN issue', - ]); - }); -}); diff --git a/src/linter/reporters/console.mjs b/src/linter/reporters/console.mjs deleted file mode 100644 index 29275472..00000000 --- a/src/linter/reporters/console.mjs +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -import { styleText } from 'node:util'; - -/** - * @type {Record} - */ -const levelToColorMap = { - info: 'gray', - warn: 'yellow', - error: 'red', -}; - -/** - * Console reporter - * - * @type {import('../types.d.ts').Reporter} - */ -export default issue => { - const position = issue.location.position - ? ` (${issue.location.position.start.line}:${issue.location.position.end.line})` - : ''; - - console[issue.level]( - styleText( - levelToColorMap[issue.level], - `${issue.message} at ${issue.location.path}${position}` - ) - ); -}; diff --git a/src/linter/reporters/github.mjs b/src/linter/reporters/github.mjs deleted file mode 100644 index 114a0550..00000000 --- a/src/linter/reporters/github.mjs +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -import * as core from '@actions/core'; - -const actions = { - warn: core.warning, - error: core.error, - info: core.notice, -}; - -/** - * GitHub actions reporter - * - * @type {import('../types.d.ts').Reporter} - */ -export default issue => { - const logFn = actions[issue.level] || core.notice; - - logFn(issue.message, { - file: issue.location.path, - startLine: issue.location.position?.start.line, - endLine: issue.location.position?.end.line, - }); -}; diff --git a/src/linter/reporters/index.mjs b/src/linter/reporters/index.mjs deleted file mode 100644 index ec85665d..00000000 --- a/src/linter/reporters/index.mjs +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -import console from './console.mjs'; -import github from './github.mjs'; - -export default { - console, - github, -}; diff --git a/src/linter/rules/__tests__/duplicate-stability-nodes.test.mjs b/src/linter/rules/__tests__/duplicate-stability-nodes.test.mjs deleted file mode 100644 index 38db1bad..00000000 --- a/src/linter/rules/__tests__/duplicate-stability-nodes.test.mjs +++ /dev/null @@ -1,226 +0,0 @@ -import { deepStrictEqual, strictEqual } from 'node:assert'; -import { describe, it } from 'node:test'; - -import { LINT_MESSAGES } from '../../constants.mjs'; -import { duplicateStabilityNodes } from '../duplicate-stability-nodes.mjs'; -import { createContext } from './utils.mjs'; - -// Mock data structure for creating test entries -const createStabilityNode = (value, line = 1, column = 1) => ({ - type: 'blockquote', - children: [ - { - type: 'paragraph', - children: [ - { - type: 'text', - value: `Stability: ${value}`, - }, - ], - }, - ], - position: { - start: { line, column }, - end: { line, column: column + 20 }, - }, -}); - -const createHeadingNode = (depth, line = 1, column = 1) => ({ - type: 'heading', - depth, - children: [ - { - type: 'text', - value: `Heading ${depth}`, - }, - ], - position: { - start: { line, column }, - end: { line, column: column + 10 }, - }, -}); - -describe('duplicateStabilityNodes', () => { - it('should not report when there are no stability nodes', () => { - const context = createContext([ - createHeadingNode(1, 1), - createHeadingNode(2, 2), - ]); - duplicateStabilityNodes(context); - strictEqual(context.report.mock.callCount(), 0); - }); - - it('should not report when there are no duplicate stability nodes', () => { - const context = createContext([ - createHeadingNode(1, 1), - createStabilityNode(0, 2), - createHeadingNode(2, 3), - createStabilityNode(1, 4), - createHeadingNode(3, 5), - createStabilityNode(2, 6), - ]); - duplicateStabilityNodes(context); - strictEqual(context.report.mock.callCount(), 0); - }); - - it('detects duplicate stability nodes within a chain', () => { - const duplicateNode = createStabilityNode(0, 4); - const context = createContext([ - createHeadingNode(1, 1), - createStabilityNode(0, 2), - createHeadingNode(2, 3), - duplicateNode, // Duplicate stability node - ]); - - duplicateStabilityNodes(context); - - strictEqual(context.report.mock.callCount(), 1); - - const call = context.report.mock.calls[0]; - - deepStrictEqual(call.arguments, [ - { - level: 'warn', - message: LINT_MESSAGES.duplicateStabilityNode, - position: duplicateNode.position, - }, - ]); - }); - - it('resets stability tracking when depth decreases', () => { - const duplicateNode1 = createStabilityNode(0, 4); - const duplicateNode2 = createStabilityNode(1, 8); - const context = createContext([ - createHeadingNode(1, 1), - createStabilityNode(0, 2), - createHeadingNode(2, 3), - duplicateNode1, // This should trigger an issue - createHeadingNode(1, 5), - createStabilityNode(1, 6), - createHeadingNode(2, 7), - duplicateNode2, // This should trigger another issue - ]); - - duplicateStabilityNodes(context); - - strictEqual(context.report.mock.callCount(), 2); - - const calls = context.report.mock.calls.flatMap(call => call.arguments); - - deepStrictEqual(calls, [ - { - level: 'warn', - message: LINT_MESSAGES.duplicateStabilityNode, - position: duplicateNode1.position, - }, - { - level: 'warn', - message: LINT_MESSAGES.duplicateStabilityNode, - position: duplicateNode2.position, - }, - ]); - }); - - it('handles missing stability nodes gracefully', () => { - const duplicateNode = createStabilityNode(0, 6); - const context = createContext([ - createHeadingNode(1, 1), - { - type: 'blockquote', - children: [ - { - type: 'paragraph', - children: [{ type: 'text', value: 'Not a stability node' }], - }, - ], - position: { start: { line: 2 }, end: { line: 2 } }, - }, - createHeadingNode(3, 3), - createStabilityNode(0, 4), - createHeadingNode(4, 5), - duplicateNode, // This should trigger an issue - ]); - - duplicateStabilityNodes(context); - - strictEqual(context.report.mock.callCount(), 1); - - const call = context.report.mock.calls[0]; - - deepStrictEqual(call.arguments, [ - { - level: 'warn', - message: LINT_MESSAGES.duplicateStabilityNode, - position: duplicateNode.position, - }, - ]); - }); - - it('handles mixed depths and stability nodes correctly', () => { - const duplicateNode1 = createStabilityNode(1, 6); - const duplicateNode2 = createStabilityNode(2, 10); - const context = createContext([ - createHeadingNode(1, 1), - createStabilityNode(0, 2), - createHeadingNode(2, 3), - createStabilityNode(1, 4), - createHeadingNode(3, 5), - duplicateNode1, // This should trigger an issue - createHeadingNode(2, 7), - createStabilityNode(2, 8), - createHeadingNode(3, 9), - duplicateNode2, // This should trigger another issue - ]); - - duplicateStabilityNodes(context); - - strictEqual(context.report.mock.callCount(), 2); - - const calls = context.report.mock.calls.flatMap(call => call.arguments); - - deepStrictEqual(calls, [ - { - level: 'warn', - message: LINT_MESSAGES.duplicateStabilityNode, - position: duplicateNode1.position, - }, - { - level: 'warn', - message: LINT_MESSAGES.duplicateStabilityNode, - position: duplicateNode2.position, - }, - ]); - }); - - it('handles malformed blockquotes gracefully', () => { - const context = createContext([ - createHeadingNode(1, 1), - { - type: 'blockquote', - children: [], // Empty children array - position: { start: { line: 2 }, end: { line: 2 } }, - }, - createHeadingNode(2, 3), - { - type: 'blockquote', - children: [{ type: 'thematicBreak' }], // No paragraph - position: { start: { line: 4 }, end: { line: 4 } }, - }, - createHeadingNode(3, 5), - { - type: 'blockquote', - children: [ - { - type: 'paragraph', - children: [], // No text node - }, - ], - position: { start: { line: 6 }, end: { line: 6 } }, - }, - ]); - - duplicateStabilityNodes(context); - - strictEqual(context.report.mock.callCount(), 0); - }); -}); diff --git a/src/linter/rules/__tests__/fixtures/invalid-change-version-subprocess.mjs b/src/linter/rules/__tests__/fixtures/invalid-change-version-subprocess.mjs deleted file mode 100644 index 7c1f4048..00000000 --- a/src/linter/rules/__tests__/fixtures/invalid-change-version-subprocess.mjs +++ /dev/null @@ -1,38 +0,0 @@ -import { deepEqual } from 'node:assert'; -import { mock } from 'node:test'; - -import dedent from 'dedent'; - -import { invalidChangeVersion } from '../../invalid-change-version.mjs'; - -const yamlContent = dedent` -`; - -const context = { - tree: { - type: 'root', - children: [ - { - type: 'html', - value: yamlContent, - position: { - start: { line: 1, column: 1, offset: 1 }, - end: { line: 1, column: 1, offset: 1 }, - }, - }, - ], - }, - report: mock.fn(), - getIssues: mock.fn(), -}; - -invalidChangeVersion(context); - -deepEqual(context.report.mock.callCount(), 0); diff --git a/src/linter/rules/__tests__/invalid-change-version.test.mjs b/src/linter/rules/__tests__/invalid-change-version.test.mjs deleted file mode 100644 index 53a0d59a..00000000 --- a/src/linter/rules/__tests__/invalid-change-version.test.mjs +++ /dev/null @@ -1,348 +0,0 @@ -import { deepStrictEqual, strictEqual } from 'node:assert'; -import { spawnSync } from 'node:child_process'; -import { execPath } from 'node:process'; -import { describe, it, mock } from 'node:test'; -import { fileURLToPath } from 'node:url'; - -import dedent from 'dedent'; - -import { invalidChangeVersion } from '../invalid-change-version.mjs'; -import { createContext } from './utils.mjs'; - -describe('invalidChangeVersion', () => { - it('should not report if all change versions are non-empty', () => { - const yamlContent = dedent` - `; - - const context = createContext([ - { - type: 'html', - value: yamlContent, - }, - ]); - - invalidChangeVersion(context); - - strictEqual(context.report.mock.callCount(), 0); - }); - - it('should report an issue if a change version is missing', () => { - const yamlContent = dedent` - `; - - const context = createContext([ - { - type: 'html', - value: yamlContent, - position: { - start: { line: 1, column: 1, offset: 1 }, - end: { line: 1, column: 1, offset: 1 }, - }, - }, - ]); - - invalidChangeVersion(context); - - strictEqual(context.report.mock.callCount(), 2); - - const callArguments = context.report.mock.calls.flatMap( - call => call.arguments - ); - - deepStrictEqual(callArguments, [ - { - level: 'error', - message: 'Missing version field in the API doc entry', - position: { - start: { line: 3 }, - end: { line: 3 }, - }, - }, - { - level: 'error', - message: 'Missing version field in the API doc entry', - position: { - start: { line: 4 }, - end: { line: 4 }, - }, - }, - ]); - }); - - it('should work with NODE_RELEASED_VERSIONS', () => { - const result = spawnSync( - execPath, - [ - fileURLToPath( - new URL( - './fixtures/invalid-change-version-subprocess.mjs', - import.meta.url - ) - ), - ], - { - env: { - NODE_RELEASED_VERSIONS: [ - '9.9.0', - '13.9.0', - '12.16.2', - '15.0.0', - 'REPLACEME', - 'SOME_OTHER_RELEASED_VERSION', - ].join(','), - }, - } - ); - - strictEqual(result.status, 0); - strictEqual(result.error, undefined); - }); - - it('should not report if all change versions are valid', () => { - const yamlContent = dedent` - `; - - const context = createContext([ - { - type: 'html', - value: yamlContent, - }, - ]); - - invalidChangeVersion(context); - - strictEqual(context.report.mock.callCount(), 0); - }); - - it('should report an issue if a change version is invalid', () => { - const yamlContent = dedent` - `; - - const context = createContext([ - { - type: 'html', - value: yamlContent, - position: { - start: { column: 1, line: 7, offset: 103 }, - end: { column: 35, line: 7, offset: 137 }, - }, - }, - ]); - - invalidChangeVersion(context); - strictEqual(context.report.mock.callCount(), 1); - const call = context.report.mock.calls[0]; - deepStrictEqual(call.arguments, [ - { - level: 'error', - message: 'Invalid version number: INVALID_VERSION', - position: { - start: { line: 11 }, - end: { line: 11 }, - }, - }, - ]); - }); - - it('should report an issue if a change version contains a REPLACEME and a version', () => { - const yamlContent = dedent` - `; - - const context = createContext([ - { - type: 'html', - value: yamlContent, - position: { - start: { column: 1, line: 7, offset: 103 }, - end: { column: 35, line: 7, offset: 137 }, - }, - }, - ]); - - invalidChangeVersion(context); - strictEqual(context.report.mock.callCount(), 1); - const call = context.report.mock.calls[0]; - deepStrictEqual(call.arguments, [ - { - level: 'error', - message: 'Invalid version number: REPLACEME', - position: { - start: { line: 11 }, - end: { line: 11 }, - }, - }, - ]); - }); - - it('should report an issue if changes is not a sequence', () => { - const yamlContent = dedent` - `; - - const context = { - tree: { - type: 'root', - children: [ - { - type: 'html', - value: yamlContent, - position: { - start: { column: 1, line: 7, offset: 103 }, - end: { column: 35, line: 7, offset: 137 }, - }, - }, - ], - }, - report: mock.fn(), - getIssues: mock.fn(), - }; - - invalidChangeVersion(context); - strictEqual(context.report.mock.callCount(), 1); - const call = context.report.mock.calls[0]; - deepStrictEqual(call.arguments, [ - { - level: 'error', - message: 'Invalid change property type', - position: { - start: { line: 8 }, - end: { line: 8 }, - }, - }, - ]); - }); - - it('should report an issue if version is not a mapping', () => { - const yamlContent = dedent` - `; - - const context = { - tree: { - type: 'root', - children: [ - { - type: 'html', - value: yamlContent, - position: { - start: { column: 1, line: 7, offset: 103 }, - end: { column: 35, line: 7, offset: 137 }, - }, - }, - ], - }, - report: mock.fn(), - getIssues: mock.fn(), - }; - - invalidChangeVersion(context); - strictEqual(context.report.mock.callCount(), 1); - const call = context.report.mock.calls[0]; - deepStrictEqual(call.arguments, [ - { - level: 'error', - message: 'Invalid change property type', - position: { - start: { line: 8 }, - end: { line: 8 }, - }, - }, - ]); - }); - - it("should skip validations if yaml root node isn't a mapping", () => { - const yamlContent = dedent` - `; - - const context = { - tree: { - type: 'root', - children: [ - { - type: 'html', - value: yamlContent, - position: { - start: { column: 1, line: 7, offset: 103 }, - end: { column: 35, line: 7, offset: 137 }, - }, - }, - ], - }, - report: mock.fn(), - getIssues: mock.fn(), - }; - - invalidChangeVersion(context); - strictEqual(context.report.mock.callCount(), 0); - }); - - it('should skip validations if changes node is missing', () => { - const yamlContent = dedent` - `; - - const context = { - tree: { - type: 'root', - children: [ - { - type: 'html', - value: yamlContent, - position: { - start: { column: 1, line: 7, offset: 103 }, - end: { column: 35, line: 7, offset: 137 }, - }, - }, - ], - }, - report: mock.fn(), - getIssues: mock.fn(), - }; - - invalidChangeVersion(context); - strictEqual(context.report.mock.callCount(), 0); - }); -}); diff --git a/src/linter/rules/__tests__/missing-metadata.test.mjs b/src/linter/rules/__tests__/missing-metadata.test.mjs deleted file mode 100644 index f6cf8d28..00000000 --- a/src/linter/rules/__tests__/missing-metadata.test.mjs +++ /dev/null @@ -1,54 +0,0 @@ -import { strictEqual } from 'node:assert'; -import { describe, it } from 'node:test'; - -import { createContext } from './utils.mjs'; -import { missingMetadata } from '../../rules/missing-metadata.mjs'; - -describe('missingMetadata', () => { - it('should not report when both fields are present', () => { - const context = createContext([ - { type: 'html', value: '' }, - { type: 'html', value: '' }, - ]); - - missingMetadata(context); - strictEqual(context.report.mock.callCount(), 0); - }); - - it('should report only llm_description when introduced_in is present', () => { - const context = createContext([ - { type: 'html', value: '' }, - ]); - - missingMetadata(context); - strictEqual(context.report.mock.callCount(), 1); - strictEqual(context.report.mock.calls[0].arguments[0].level, 'warn'); - }); - - it('should not report llm_description when paragraph fallback exists', () => { - const context = createContext([ - { type: 'html', value: '' }, - { type: 'paragraph', children: [{ type: 'text', value: 'desc' }] }, - ]); - - missingMetadata(context); - strictEqual(context.report.mock.callCount(), 0); - }); - - it('should report both when nothing is present', () => { - const context = createContext([{ type: 'heading', depth: 1 }]); - - missingMetadata(context); - strictEqual(context.report.mock.callCount(), 2); - }); - - it('should report only introduced_in when llm_description is present', () => { - const context = createContext([ - { type: 'html', value: '' }, - ]); - - missingMetadata(context); - strictEqual(context.report.mock.callCount(), 1); - strictEqual(context.report.mock.calls[0].arguments[0].level, 'info'); - }); -}); diff --git a/src/linter/rules/__tests__/utils.mjs b/src/linter/rules/__tests__/utils.mjs deleted file mode 100644 index 22c8d2cd..00000000 --- a/src/linter/rules/__tests__/utils.mjs +++ /dev/null @@ -1,7 +0,0 @@ -import { mock } from 'node:test'; - -export const createContext = children => ({ - tree: { type: 'root', children }, - report: mock.fn(), - getIssues: mock.fn(), -}); diff --git a/src/linter/rules/duplicate-stability-nodes.mjs b/src/linter/rules/duplicate-stability-nodes.mjs deleted file mode 100644 index 8c32bbb1..00000000 --- a/src/linter/rules/duplicate-stability-nodes.mjs +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -import { visit } from 'unist-util-visit'; - -import createQueries from '../../utils/queries/index.mjs'; -import { LINT_MESSAGES } from '../constants.mjs'; - -/** - * Checks if there are multiple stability nodes within a chain. - * - * @param {import('../types.d.ts').LintContext} context - * @returns {void} - */ -export const duplicateStabilityNodes = context => { - let currentDepth = 0; - let currentStability = -1; - let currentHeaderDepth = 0; - - visit(context.tree, node => { - // Track the current header depth - if (node.type === 'heading') { - currentHeaderDepth = node.depth; - } - - // Process blockquotes to find stability nodes - if (node.type === 'blockquote') { - if (node.children && node.children.length > 0) { - const paragraph = node.children[0]; - if ( - paragraph.type === 'paragraph' && - paragraph.children && - paragraph.children.length > 0 - ) { - const text = paragraph.children[0]; - if (text.type === 'text') { - const match = text.value.match( - createQueries.QUERIES.stabilityIndex - ); - if (match) { - const stability = parseFloat(match[1]); - - if ( - currentHeaderDepth > currentDepth && - stability >= 0 && - stability === currentStability - ) { - context.report({ - level: 'warn', - message: LINT_MESSAGES.duplicateStabilityNode, - position: node.position, - }); - } else { - currentDepth = currentHeaderDepth; - currentStability = stability; - } - } - } - } - } - } - }); -}; diff --git a/src/linter/rules/index.mjs b/src/linter/rules/index.mjs deleted file mode 100644 index 09e0e462..00000000 --- a/src/linter/rules/index.mjs +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -import { duplicateStabilityNodes } from './duplicate-stability-nodes.mjs'; -import { invalidChangeVersion } from './invalid-change-version.mjs'; -import { missingMetadata } from './missing-metadata.mjs'; - -/** - * @type {Record} - */ -export default { - 'duplicate-stability-nodes': duplicateStabilityNodes, - 'invalid-change-version': invalidChangeVersion, - 'missing-metadata': missingMetadata, -}; diff --git a/src/linter/rules/invalid-change-version.mjs b/src/linter/rules/invalid-change-version.mjs deleted file mode 100644 index 47a54f5c..00000000 --- a/src/linter/rules/invalid-change-version.mjs +++ /dev/null @@ -1,145 +0,0 @@ -import { env } from 'node:process'; - -import { valid, parse } from 'semver'; -import { visit } from 'unist-util-visit'; -import { isMap, isSeq, LineCounter, parseDocument } from 'yaml'; - -import { - extractYamlContent, - normalizeYamlSyntax, -} from '../../utils/parser/index.mjs'; -import createQueries from '../../utils/queries/index.mjs'; -import { LINT_MESSAGES } from '../constants.mjs'; -import { - createYamlIssueReporter, - findPropertyByName, - normalizeNode, -} from '../utils/yaml.mjs'; - -const NODE_RELEASED_VERSIONS = env.NODE_RELEASED_VERSIONS?.split(','); - -/** - * Checks if the given version is "REPLACEME" and the array length is 1. - * - * @param {string} version - The version to check. - * @param {number} length - Length of the version array. - * @returns {boolean} True if conditions match, otherwise false. - */ -const isValidReplaceMe = (version, length) => - length === 1 && version === 'REPLACEME'; - -/** - * Checks if a given semantic version should be ignored. - * A version is considered ignored if its major version is 0 and minor version is less than 2. - * - * These versions are extremely old, and are not shown in the changelog used to generate - * `NODE_RELEASED_VERSIONS`, so they must be hardcoded. - * - * @param {string} version - The version to check. - * @returns {boolean} Returns true if the version is ignored, false otherwise. - */ -const isIgnoredVersion = version => { - const { major, minor } = parse(version) || {}; - return major === 0 && minor < 2; -}; - -/** - * Determines if a given version is invalid. - * - * @param {import('yaml').Scalar} version - The version to check. - * @param {unknown} _ - Unused parameter. - * @param {{ length: number }} context - Array containing the length property. - * @returns {boolean} True if the version is invalid, otherwise false. - */ -const isInvalid = NODE_RELEASED_VERSIONS - ? ({ value }, _, { length }) => - !( - isValidReplaceMe(value, length) || - isIgnoredVersion(value) || - NODE_RELEASED_VERSIONS.includes(value.replace(/^v/, '')) - ) - : ({ value }, _, { length }) => - !(isValidReplaceMe(value, length) || valid(value)); - -/** - * Validates and extracts versions of a change node - * - * @param {object} root0 - * @param {import('../types.d.ts').LintContext} root0.context - * @param {import('yaml').Node} root0.node - * @param {(message: string, node: import('yaml').Node) => import('../types.d.ts').IssueDescriptor} root0.createYamlIssue - */ -export const extractVersions = ({ context, node, createYamlIssue }) => { - if (!isMap(node)) { - context.report(createYamlIssue(LINT_MESSAGES.invalidChangeProperty, node)); - - return []; - } - - const versionNode = findPropertyByName(node, 'version'); - - if (!versionNode) { - context.report(createYamlIssue(LINT_MESSAGES.missingChangeVersion, node)); - - return []; - } - - return normalizeNode(versionNode.value); -}; - -/** - * Identifies invalid change versions from metadata entries. - * - * @param {import('../types.d.ts').LintContext} context - * @returns {void} - */ -export const invalidChangeVersion = context => { - visit(context.tree, createQueries.UNIST.isYamlNode, node => { - const yamlContent = extractYamlContent(node); - - const normalizedYaml = normalizeYamlSyntax(yamlContent); - - const lineCounter = new LineCounter(); - const document = parseDocument(normalizedYaml, { lineCounter }); - - const createYamlIssue = createYamlIssueReporter(node, lineCounter); - - // Skip if yaml isn't a mapping - if (!isMap(document.contents)) { - return; - } - - const changesNode = findPropertyByName(document.contents, 'changes'); - - // Skip if changes node is not present - if (!changesNode) { - return; - } - - // Validate changes node is a sequence - if (!isSeq(changesNode.value)) { - return context.report( - createYamlIssue(LINT_MESSAGES.invalidChangeProperty, changesNode.key) - ); - } - - changesNode.value.items.forEach(node => { - extractVersions({ context, node, createYamlIssue }) - .filter(Boolean) // Filter already reported empt items, - .filter(isInvalid) - .forEach(version => - context.report( - createYamlIssue( - version?.value - ? LINT_MESSAGES.invalidChangeVersion.replace( - '{{version}}', - version.value - ) - : LINT_MESSAGES.missingChangeVersion, - version - ) - ) - ); - }); - }); -}; diff --git a/src/linter/rules/missing-metadata.mjs b/src/linter/rules/missing-metadata.mjs deleted file mode 100644 index f07e3366..00000000 --- a/src/linter/rules/missing-metadata.mjs +++ /dev/null @@ -1,77 +0,0 @@ -'use strict'; - -import { find } from 'unist-util-find'; -import { findBefore } from 'unist-util-find-before'; - -import { - INTRODUCED_IN_REGEX, - LINT_MESSAGES, - LLM_DESCRIPTION_REGEX, -} from '../constants.mjs'; - -/** - * Finds the first node that matches the condition before the first h2 heading, - * this area is considered the top-level section of the tree - * - * @param {import('mdast').Node} node - * @param {import('unist-util-find').TestFn} condition - */ -const findTopLevelEntry = (node, condition) => { - const h2 = find(node, { type: 'heading', depth: 2 }); - return h2 ? findBefore(node, h2, condition) : find(node, condition); -}; - -// Simplified metadata checks - llmDescription can fall back to paragraph -const METADATA_CHECKS = Object.freeze([ - { - name: 'introducedIn', - regex: INTRODUCED_IN_REGEX, - level: 'info', - message: LINT_MESSAGES.missingIntroducedIn, - }, - { - name: 'llmDescription', - regex: LLM_DESCRIPTION_REGEX, - level: 'warn', - message: LINT_MESSAGES.missingLlmDescription, - }, -]); - -/** - * Checks if required metadata fields are missing in the top-level entry. - * - * @param {import('../types.d.ts').LintContext} context - * @returns {void} - */ -export const missingMetadata = context => { - const foundMetadata = new Set(); - let hasParagraph = false; - - findTopLevelEntry(context.tree, node => { - if (node.type === 'html') { - for (const check of METADATA_CHECKS) { - if (check.regex?.test(node.value)) { - foundMetadata.add(check.name); - } - } - } else if (node.type === 'paragraph') { - hasParagraph = true; - } - return false; // Continue searching - }); - - // Report missing metadata - for (const check of METADATA_CHECKS) { - if (!foundMetadata.has(check.name)) { - // The first paragraph can also be the LLM description. - if (check.name === 'llmDescription' && hasParagraph) { - continue; - } - - context.report({ - level: check.level, - message: check.message, - }); - } - } -}; diff --git a/src/linter/types.d.ts b/src/linter/types.d.ts deleted file mode 100644 index 41e64cf9..00000000 --- a/src/linter/types.d.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Root } from 'mdast'; -import reporters from './reporters/index.mjs'; -import { VFile } from 'vfile'; - -export type IssueLevel = 'info' | 'warn' | 'error'; - -export interface Position { - start: { line: number }; - end: { line: number }; -} - -export interface LintIssueLocation { - path: string; // The absolute path to the file - position?: Position; -} - -export interface LintIssue { - level: IssueLevel; - message: string; - location: LintIssueLocation; -} - -type LintRule = (context: LintContext) => void; - -export type Reporter = (message: LintIssue) => void; - -export interface Linter { - readonly issues: LintIssue[]; - lint: (file: VFile, tree: Root) => void; - report: (reporterName: keyof typeof reporters) => void; - hasError: () => boolean; -} - -export interface IssueDescriptor { - level: IssueLevel; - message: string; - position?: Position; -} - -export interface LintContext { - readonly tree: Root; - report(descriptor: IssueDescriptor): void; - getIssues(): LintIssue[]; -} diff --git a/src/linter/utils/__tests__/yaml.mjs b/src/linter/utils/__tests__/yaml.mjs deleted file mode 100644 index ec715053..00000000 --- a/src/linter/utils/__tests__/yaml.mjs +++ /dev/null @@ -1,99 +0,0 @@ -import { deepStrictEqual, strictEqual, throws } from 'node:assert'; -import { describe, it } from 'node:test'; - -import { Scalar, Pair, YAMLSeq, YAMLMap } from 'yaml'; - -import { findPropertyByName, normalizeNode } from '../yaml.mjs'; - -describe('yaml', () => { - describe('findPropertyByName', () => { - it('should find a property by name when it exists', () => { - const mockMap = { - items: [ - new Pair(new Scalar('propertyA'), new Scalar('valueA')), - new Pair(new Scalar('propertyB'), new Scalar('valueB')), - ], - }; - - const result = findPropertyByName(mockMap, 'propertyA'); - - strictEqual(result.key.value, 'propertyA'); - strictEqual(result.value.value, 'valueA'); - }); - - it('should return undefined when property does not exist', () => { - const mockMap = { - items: [ - new Pair(new Scalar('propertyA'), new Scalar('valueA')), - new Pair(new Scalar('propertyB'), new Scalar('valueB')), - ], - }; - - const result = findPropertyByName(mockMap, 'nonExistent'); - strictEqual(result, undefined); - }); - }); - - describe('normalizeNode', () => { - it('should normalize a scalar node', () => { - const scalar = new Scalar('test-value'); - scalar.range = [0, 10, 10]; - - const result = normalizeNode(scalar); - - deepStrictEqual(result, [ - { - value: 'test-value', - range: [0, 10, 10], - }, - ]); - }); - - it('should normalize a sequence with scalar items', () => { - const item1 = new Scalar('first'); - item1.range = [0, 5, 5]; - - const item2 = new Scalar('second'); - item2.range = [6, 12, 12]; - - const sequence = new YAMLSeq(); - sequence.items = [item1, item2]; - - const result = normalizeNode(sequence); - - deepStrictEqual(result, [ - { value: 'first', range: [0, 5, 5] }, - { value: 'second', range: [6, 12, 12] }, - ]); - }); - - it('should normalize nested sequences', () => { - const innerItem = new Scalar('nested'); - innerItem.range = [0, 6, 6]; - - const innerSeq = new YAMLSeq(); - innerSeq.items = [innerItem]; - - const outerItem = new Scalar('outer'); - outerItem.range = [6, 12, 12]; - - const outerSeq = new YAMLSeq(); - outerSeq.items = [outerItem, innerSeq]; - - const result = normalizeNode(outerSeq); - - deepStrictEqual(result, [ - { value: 'outer', range: [6, 12, 12] }, - { value: 'nested', range: [0, 6, 6] }, - ]); - }); - - it('should throw error for map nodes', () => { - const mapNode = new YAMLMap(); - - throws(() => normalizeNode(mapNode), { - message: 'Unexpected node type: map', - }); - }); - }); -}); diff --git a/src/linter/utils/rules.mjs b/src/linter/utils/rules.mjs deleted file mode 100644 index 22479f75..00000000 --- a/src/linter/utils/rules.mjs +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -import rules from '../rules/index.mjs'; - -/** - * Gets all enabled rules - * - * @param {string[]} [disabledRules] - List of disabled rule names - * @returns {import('../types').LintRule[]} - List of enabled rules - */ -export const getEnabledRules = (disabledRules = []) => { - return Object.entries(rules) - .filter(([ruleName]) => !disabledRules.includes(ruleName)) - .map(([, rule]) => rule); -}; diff --git a/src/linter/utils/yaml.mjs b/src/linter/utils/yaml.mjs deleted file mode 100644 index 63df8ba8..00000000 --- a/src/linter/utils/yaml.mjs +++ /dev/null @@ -1,77 +0,0 @@ -// @ts-check - -'use strict'; - -import { isScalar, isSeq, isNode } from 'yaml'; - -/** - * Searches for a property by name in a map node. - * - * @param {import('yaml').YAMLMap} node - The map node to search in. - * @param {string} propertyName - The property name to search for. - * @returns {import('yaml').Pair | undefined} - */ -export const findPropertyByName = (node, propertyName) => { - return node.items.find(pair => { - if (!isScalar(pair.key)) { - return; - } - - return pair.key.value === propertyName; - }); -}; - -/** - * Normalizes a YAML node values into an array. - * - * @param {import('yaml').Node} node - * @returns {import('yaml').Scalar[]} - */ -export const normalizeNode = node => { - if (isScalar(node)) { - return [node]; - } - - if (isSeq(node)) { - // @ts-ignore - return node.items.flatMap(item => normalizeNode(item)); - } - - throw new Error(`Unexpected node type: map`); -}; - -/** - * Creates a factory function for generating error descriptors with proper line positioning - * for YAML nodes within markdown documents. - * - * @param {import('mdast').Node} yamlNode - * @param {import('yaml').LineCounter} lineCounter - */ -export const createYamlIssueReporter = (yamlNode, lineCounter) => { - const initialLine = yamlNode.position?.start.line ?? 0; - - /** - * @param {string} message - * @param {import('yaml').Node} node - * @returns {import('../types').IssueDescriptor} - */ - return (message, node) => { - const absoluteLine = - isNode(node) && node.range - ? initialLine + lineCounter.linePos(node.range[0]).line - : initialLine; - - return { - level: 'error', - message, - position: { - start: { - line: absoluteLine, - }, - end: { - line: absoluteLine, - }, - }, - }; - }; -}; diff --git a/src/parsers/markdown.mjs b/src/parsers/markdown.mjs index 608c783e..cce3618b 100644 --- a/src/parsers/markdown.mjs +++ b/src/parsers/markdown.mjs @@ -17,10 +17,8 @@ const NODE_LTS_VERSION_REGEX = /Long Term Support/i; /** * Creates an API doc parser for a given Markdown API doc file - * - * @param {import('../linter/types').Linter} [linter] */ -const createParser = linter => { +const createParser = () => { // Creates an instance of the Remark processor with GFM support const remarkProcessor = getRemark(); @@ -43,8 +41,6 @@ const createParser = linter => { // Parses the API doc into an AST tree using `unified` and `remark` const apiDocTree = remarkProcessor.parse(resolvedApiDoc); - linter?.lint(resolvedApiDoc, apiDocTree); - return { file: { stem: resolvedApiDoc.stem,