Skip to content

feat: introduce raw mdast linter #275

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
13 changes: 9 additions & 4 deletions bin/commands/generate.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { cpus } from 'node:os';
import { resolve } from 'node:path';
import process from 'node:process';

import { coerce } from 'semver';

Expand All @@ -12,7 +11,8 @@ import createGenerator from '../../src/generators.mjs';
import { publicGenerators } from '../../src/generators/index.mjs';
import createNodeReleases from '../../src/releases.mjs';
import { loadAndParse } from '../utils.mjs';
import { runLint } from './lint.mjs';
import createLinter from '../../src/linter/index.mjs';
import { getEnabledRules } from '../../src/linter/utils/rules.mjs';

const availableGenerators = Object.keys(publicGenerators);

Expand Down Expand Up @@ -123,9 +123,14 @@ export default {
* @returns {Promise<void>}
*/
async action(opts) {
const docs = await loadAndParse(opts.input, opts.ignore);
const rules = getEnabledRules(opts.disableRule);
const linter = opts.skipLint ? undefined : createLinter(rules);

if (!opts.skipLint && !runLint(docs)) {
const docs = await loadAndParse(opts.input, opts.ignore, linter);

linter?.report();

if (linter?.hasError()) {
console.error('Lint failed; aborting generation.');
process.exit(1);
}
Expand Down
28 changes: 9 additions & 19 deletions bin/commands/lint.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import createLinter from '../../src/linter/index.mjs';
import reporters from '../../src/linter/reporters/index.mjs';
import rules from '../../src/linter/rules/index.mjs';
import { loadAndParse } from '../utils.mjs';
import { getEnabledRules } from '../../src/linter/utils/rules.mjs';

const availableRules = Object.keys(rules);
const availableReporters = Object.keys(reporters);
Expand All @@ -17,22 +18,6 @@ const availableReporters = Object.keys(reporters);
* @property {keyof reporters} reporter - Reporter for linter output.
*/

/**
* Run the linter on parsed documentation.
* @param {ApiDocMetadataEntry[]} docs - Parsed documentation objects.
* @param {LinterOptions} options - Linter configuration options.
* @returns {boolean} - True if no errors, false otherwise.
*/
export function runLint(
docs,
{ disableRule = [], dryRun = false, reporter = 'console' } = {}
) {
const linter = createLinter(dryRun, disableRule);
linter.lintAll(docs);
linter.report(reporter);
return !linter.hasError();
}

/**
* @type {import('../utils.mjs').Command}
*/
Expand Down Expand Up @@ -95,9 +80,14 @@ export default {
*/
async action(opts) {
try {
const docs = await loadAndParse(opts.input, opts.ignore);
const success = runLint(docs, opts);
process.exitCode = success ? 0 : 1;
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;
Expand Down
7 changes: 4 additions & 3 deletions bin/utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import createMarkdownParser from '../src/parsers/markdown.mjs';
*/
export const lazy = factory => {
let instance;
return () => (instance ??= factory());
return args => (instance ??= factory(args));
};

// Instantiate loader and parser once to reuse,
Expand All @@ -23,11 +23,12 @@ 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<ApiDocMetadataEntry[]>} - Parsed documentation objects.
*/
export async function loadAndParse(input, ignore) {
export async function loadAndParse(input, ignore, linter) {
const files = await loader().loadFiles(input, ignore);
return parser().parseApiDocs(files);
return parser(linter).parseApiDocs(files);
}

/**
Expand Down
37 changes: 37 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@
"shiki": "^3.2.1",
"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",
Expand Down
9 changes: 1 addition & 8 deletions src/generators/legacy-json/utils/buildSection.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,7 @@ import { getRemarkRehype } from '../../../utils/remark.mjs';
import { transformNodesToString } from '../../../utils/unist.mjs';
import { parseList } from './parseList.mjs';
import { SECTION_TYPE_PLURALS, UNPROMOTED_KEYS } from '../constants.mjs';

/**
* Converts a value to an array.
* @template T
* @param {T | T[]} val - The value to convert.
* @returns {T[]} The value as an array.
*/
const enforceArray = val => (Array.isArray(val) ? val : [val]);
import { enforceArray } from '../../../utils/array.mjs';

/**
*
Expand Down
4 changes: 4 additions & 0 deletions src/linter/constants.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
'use strict';

export const INTRDOCUED_IN_REGEX = /<!--\s?introduced_in=.*-->/;

export const LLM_DESCRIPTION_REGEX = /<!--\s?llm_description=.*-->/;

export const LINT_MESSAGES = {
missingIntroducedIn: "Missing 'introduced_in' field in the API doc entry",
missingChangeVersion: 'Missing version field in the API doc entry',
Expand Down
56 changes: 56 additions & 0 deletions src/linter/context.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'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;
30 changes: 0 additions & 30 deletions src/linter/engine.mjs

This file was deleted.

55 changes: 26 additions & 29 deletions src/linter/index.mjs
Original file line number Diff line number Diff line change
@@ -1,52 +1,48 @@
'use strict';

import createLinterEngine from './engine.mjs';
import createContext from './context.mjs';
import reporters from './reporters/index.mjs';
import rules from './rules/index.mjs';

/**
* Creates a linter instance to validate ApiDocMetadataEntry entries
* Creates a linter instance to validate API documentation ASTs against a
* defined set of rules.
*
* @param {boolean} dryRun Whether to run the engine in dry-run mode
* @param {string[]} disabledRules List of disabled rules names
* @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 = (dryRun, disabledRules) => {
const createLinter = (rules, dryRun = false) => {
/**
* Retrieves all enabled rules
*
* @returns {import('./types').LintRule[]}
*/
const getEnabledRules = () => {
return Object.entries(rules)
.filter(([ruleName]) => !disabledRules.includes(ruleName))
.map(([, rule]) => rule);
};

const engine = createLinterEngine(getEnabledRules(disabledRules));

/**
* Lint issues found during validations
* Lint issues collected during validations.
*
* @type {Array<import('./types').LintIssue>}
*/
const issues = [];

/**
* Lints all entries using the linter engine
* Lints a API doc and collects issues.
*
* @param entries
* @param {import('vfile').VFile} file
* @param {import('mdast').Root} tree
* @returns {void}
*/
const lintAll = entries => {
issues.push(...engine.lintAll(entries));
const lint = (file, tree) => {
const context = createContext(file, tree);

for (const rule of rules) {
rule(context);
}

issues.push(...context.getIssues());
};

/**
* Reports found issues using the specified reporter
* Reports collected issues using the specified reporter.
*
* @param {keyof typeof reporters} reporterName Reporter name
* @param {keyof typeof reporters} [reporterName] Reporter name
* @returns {void}
*/
const report = reporterName => {
const report = (reporterName = 'console') => {
if (dryRun) {
return;
}
Expand All @@ -59,7 +55,7 @@ const createLinter = (dryRun, disabledRules) => {
};

/**
* Checks if any error-level issues were found during linting
* Checks if any error-level issues were collected.
*
* @returns {boolean}
*/
Expand All @@ -68,7 +64,8 @@ const createLinter = (dryRun, disabledRules) => {
};

return {
lintAll,
issues,
lint,
report,
hasError,
};
Expand Down
Loading
Loading