From 63e0d97b3c27a455ebe84b23148eb263ff0644bd Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Mon, 3 Feb 2025 13:47:00 -0800 Subject: [PATCH 01/22] feat: linter Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- bin/cli.mjs | 31 +++++++++++- src/generators/types.d.ts | 3 ++ src/linter/index.mjs | 81 ++++++++++++++++++++++++++++++++ src/linter/reporters/console.mjs | 22 +++++++++ src/linter/reporters/github.mjs | 13 +++++ src/linter/reporters/index.mjs | 9 ++++ src/linter/types.d.ts | 16 +++++++ src/parsers/markdown.mjs | 6 ++- 8 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 src/linter/index.mjs create mode 100644 src/linter/reporters/console.mjs create mode 100644 src/linter/reporters/github.mjs create mode 100644 src/linter/reporters/index.mjs create mode 100644 src/linter/types.d.ts diff --git a/bin/cli.mjs b/bin/cli.mjs index ba6f8eaa..15ebbd10 100755 --- a/bin/cli.mjs +++ b/bin/cli.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node import { resolve } from 'node:path'; -import { argv } from 'node:process'; +import { argv, exit } from 'node:process'; import { Command, Option } from 'commander'; @@ -12,6 +12,8 @@ import generators from '../src/generators/index.mjs'; import createMarkdownLoader from '../src/loaders/markdown.mjs'; import createMarkdownParser from '../src/parsers/markdown.mjs'; import createNodeReleases from '../src/releases.mjs'; +import { Linter } from '../src/linter/index.mjs'; +import reporters from '../src/linter/reporters/index.mjs'; const availableGenerators = Object.keys(generators); @@ -50,6 +52,12 @@ program 'Set the processing target modes' ).choices(availableGenerators) ) + .addOption(new Option('--skip-validation', 'TODO').default(false)) + .addOption( + new Option('--reporter', 'TODO') + .choices(Object.keys(reporters)) + .default('console') + ) .parse(argv); /** @@ -66,7 +74,17 @@ program * @type {Options} * @description The return type for values sent to the program from the CLI. */ -const { input, output, target = [], version, changelog } = program.opts(); +const { + input, + output, + target = [], + version, + changelog, + skipValidation, + reporter, +} = program.opts(); + +const linter = skipValidation ? undefined : new Linter(); const { loadFiles } = createMarkdownLoader(); const { parseApiDocs } = createMarkdownParser(); @@ -91,4 +109,13 @@ await runGenerators({ version: coerce(version), // A list of all Node.js major versions with LTS status releases: await getAllMajors(), + linter, }); + +if (linter) { + linter.report(reporter); + + if (linter.hasError) { + exit(1); + } +} diff --git a/src/generators/types.d.ts b/src/generators/types.d.ts index 110b5dc0..e56e84b8 100644 --- a/src/generators/types.d.ts +++ b/src/generators/types.d.ts @@ -1,6 +1,7 @@ import type { SemVer } from 'semver'; import type availableGenerators from './index.mjs'; import type { ApiDocReleaseEntry } from '../types'; +import type { Linter } from '../linter/index.mjs'; declare global { // All available generators as an inferable type, to allow Generator interfaces @@ -30,6 +31,8 @@ declare global { // A list of all Node.js major versions and their respective release information releases: Array; + + linter: Linter | undefined; } export interface GeneratorMetadata { diff --git a/src/linter/index.mjs b/src/linter/index.mjs new file mode 100644 index 00000000..4f379349 --- /dev/null +++ b/src/linter/index.mjs @@ -0,0 +1,81 @@ +// @ts-check +'use strict'; + +import reporters from './reporters/index.mjs'; + +/** + * + */ +export class Linter { + #_hasError = false; + + /** + * @type {Array} + */ + #messages = []; + + /** + * + */ + get hasError() { + return this.#_hasError; + } + + /** + * @param {import('./types.d.ts').LintMessage} msg + */ + log(msg) { + if (msg.level === 'error') { + this.#_hasError = true; + } + + this.#messages.push(msg); + } + + /** + * @param {keyof reporters} reporterName + */ + report(reporterName) { + const reporter = reporters[reporterName]; + + for (const message of this.#messages) { + reporter(message); + } + } + + /** + * @param {string} msg + * @param {import('./types.d.ts').LintMessageLocation | undefined} location + */ + info(msg, location) { + this.log({ + level: 'info', + msg, + location, + }); + } + + /** + * @param {string} msg + * @param {import('./types.d.ts').LintMessageLocation | undefined} location + */ + warn(msg, location) { + this.log({ + level: 'warn', + msg, + location, + }); + } + + /** + * @param {string} msg + * @param {import('./types.d.ts').LintMessageLocation | undefined} location + */ + error(msg, location) { + this.log({ + level: 'error', + msg, + location, + }); + } +} diff --git a/src/linter/reporters/console.mjs b/src/linter/reporters/console.mjs new file mode 100644 index 00000000..394d0606 --- /dev/null +++ b/src/linter/reporters/console.mjs @@ -0,0 +1,22 @@ +// @ts-check + +'use strict'; + +import { styleText } from 'node:util'; + +/** + * TODO is there a way to grab the parameter type for styleText since the types aren't exported + * @type {Record} + */ +const levelToColorMap = { + info: 'gray', + warn: 'yellow', + error: 'red', +}; + +/** + * @type {import('../types.d.ts').Reporter} + */ +export default msg => { + console.log(styleText(levelToColorMap[msg.level], msg.msg)); +}; diff --git a/src/linter/reporters/github.mjs b/src/linter/reporters/github.mjs new file mode 100644 index 00000000..fdd005ab --- /dev/null +++ b/src/linter/reporters/github.mjs @@ -0,0 +1,13 @@ +// @ts-check + +'use strict'; + +/** + * GitHub action reporter for + * + * @type {import('../types.d.ts').Reporter} + */ +export default msg => { + // TODO + console.log(msg); +}; diff --git a/src/linter/reporters/index.mjs b/src/linter/reporters/index.mjs new file mode 100644 index 00000000..efaa58f4 --- /dev/null +++ b/src/linter/reporters/index.mjs @@ -0,0 +1,9 @@ +'use strict'; + +import console from './console.mjs'; +import github from './github.mjs'; + +export default /** @type {const} */ ({ + console, + github, +}); diff --git a/src/linter/types.d.ts b/src/linter/types.d.ts new file mode 100644 index 00000000..886ba023 --- /dev/null +++ b/src/linter/types.d.ts @@ -0,0 +1,16 @@ +export type LintLevel = 'info' | 'warn' | 'error'; + +export interface LintMessageLocation { + // The absolute path to the file + path: string; + line: number; + column: number; +} + +export interface LintMessage { + level: LintLevel; + msg: string; + location?: LintMessageLocation; +} + +export type Reporter = (msg: LintMessage) => void; diff --git a/src/parsers/markdown.mjs b/src/parsers/markdown.mjs index 65707e38..a0110f88 100644 --- a/src/parsers/markdown.mjs +++ b/src/parsers/markdown.mjs @@ -14,12 +14,14 @@ import { createNodeSlugger } from '../utils/slugger.mjs'; /** * Creates an API doc parser for a given Markdown API doc file + * + * @param {import('./linter/index.mjs').Linter | undefined} linter */ -const createParser = () => { +const createParser = linter => { // Creates an instance of the Remark processor with GFM support // which is used for stringifying the AST tree back to Markdown const remarkProcessor = getRemark(); - + linter?.info('asd123'); const { setHeadingMetadata, addYAMLMetadata, From 00362609648c41afb0c705e4e19bdd669460b0f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Mon, 17 Feb 2025 12:41:25 -0300 Subject: [PATCH 02/22] feat: wip --- bin/cli.mjs | 7 +- package-lock.json | 66 +++++++++++++++++ package.json | 1 + src/linter/index.mjs | 78 +++++++-------------- src/linter/reporters/console.mjs | 13 ++-- src/linter/reporters/github.mjs | 17 ++++- src/linter/rules/invalid-change-version.mjs | 33 +++++++++ src/linter/rules/missing-change-version.mjs | 23 ++++++ src/linter/rules/missing-introduced-in.mjs | 24 +++++++ src/linter/types.d.ts | 24 ++++--- src/linter/utils/semver.mjs | 12 ++++ src/metadata.mjs | 10 +++ src/parsers/markdown.mjs | 12 ++-- src/types.d.ts | 3 +- 14 files changed, 245 insertions(+), 78 deletions(-) create mode 100644 src/linter/rules/invalid-change-version.mjs create mode 100644 src/linter/rules/missing-change-version.mjs create mode 100644 src/linter/rules/missing-introduced-in.mjs create mode 100644 src/linter/utils/semver.mjs diff --git a/bin/cli.mjs b/bin/cli.mjs index 15ebbd10..3325a2ce 100755 --- a/bin/cli.mjs +++ b/bin/cli.mjs @@ -52,9 +52,9 @@ program 'Set the processing target modes' ).choices(availableGenerators) ) - .addOption(new Option('--skip-validation', 'TODO').default(false)) + .addOption(new Option('--skip-linting', 'Skip linting').default(false)) .addOption( - new Option('--reporter', 'TODO') + new Option('--reporter', 'Specify the linter reporter') .choices(Object.keys(reporters)) .default('console') ) @@ -98,6 +98,8 @@ const { runGenerators } = createGenerator(parsedApiDocs); // Retrieves Node.js release metadata from a given Node.js version and CHANGELOG.md file const { getAllMajors } = createNodeReleases(changelog); +linter?.lintAll(parsedApiDocs); + await runGenerators({ // A list of target modes for the API docs parser generators: target, @@ -109,7 +111,6 @@ await runGenerators({ version: coerce(version), // A list of all Node.js major versions with LTS status releases: await getAllMajors(), - linter, }); if (linter) { diff --git a/package-lock.json b/package-lock.json index 777215e9..8e0f7735 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,7 @@ "": { "name": "@node-core/api-docs-tooling", "dependencies": { + "@actions/core": "^1.11.1", "acorn": "^8.14.0", "commander": "^13.1.0", "dedent": "^1.5.3", @@ -48,6 +49,41 @@ "prettier": "3.4.2" } }, + "node_modules/@actions/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "license": "MIT", + "dependencies": { + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "license": "MIT", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/http-client": { + "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==", + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "license": "MIT" + }, "node_modules/@es-joy/jsdoccomment": { "version": "0.49.0", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.49.0.tgz", @@ -201,6 +237,15 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -3925,6 +3970,15 @@ "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "license": "0BSD" }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3938,6 +3992,18 @@ "node": ">= 0.8.0" } }, + "node_modules/undici": { + "version": "5.28.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz", + "integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", diff --git a/package.json b/package.json index e335033d..d2c933a4 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "acorn": "^8.14.0", + "@actions/core": "^1.11.1", "commander": "^13.1.0", "estree-util-visit": "^2.0.0", "dedent": "^1.5.3", diff --git a/src/linter/index.mjs b/src/linter/index.mjs index 4f379349..656203be 100644 --- a/src/linter/index.mjs +++ b/src/linter/index.mjs @@ -2,34 +2,46 @@ 'use strict'; import reporters from './reporters/index.mjs'; +import { invalidChangeVersion } from './rules/invalid-change-version.mjs'; +import { missingChangeVersion } from './rules/missing-change-version.mjs'; +import { missingIntroducedIn } from './rules/missing-introduced-in.mjs'; /** - * + * Lint issues in ApiDocMetadataEntry entries */ export class Linter { - #_hasError = false; + /** + * @type {Array} + */ + #issues = []; /** - * @type {Array} + * @type {Array} */ - #messages = []; + #rules = [missingIntroducedIn, missingChangeVersion, invalidChangeVersion]; /** - * + * @param {ApiDocMetadataEntry} entry + * @returns {void} */ - get hasError() { - return this.#_hasError; + lint(entry) { + for (const rule of this.#rules) { + const issues = rule(entry); + + if (issues.length > 0) { + this.#issues.push(...issues); + } + } } /** - * @param {import('./types.d.ts').LintMessage} msg + * @param {ApiDocMetadataEntry[]} entries + * @returns {void} */ - log(msg) { - if (msg.level === 'error') { - this.#_hasError = true; + lintAll(entries) { + for (const entry of entries) { + this.lint(entry); } - - this.#messages.push(msg); } /** @@ -38,44 +50,8 @@ export class Linter { report(reporterName) { const reporter = reporters[reporterName]; - for (const message of this.#messages) { - reporter(message); + for (const issue of this.#issues) { + reporter(issue); } } - - /** - * @param {string} msg - * @param {import('./types.d.ts').LintMessageLocation | undefined} location - */ - info(msg, location) { - this.log({ - level: 'info', - msg, - location, - }); - } - - /** - * @param {string} msg - * @param {import('./types.d.ts').LintMessageLocation | undefined} location - */ - warn(msg, location) { - this.log({ - level: 'warn', - msg, - location, - }); - } - - /** - * @param {string} msg - * @param {import('./types.d.ts').LintMessageLocation | undefined} location - */ - error(msg, location) { - this.log({ - level: 'error', - msg, - location, - }); - } } diff --git a/src/linter/reporters/console.mjs b/src/linter/reporters/console.mjs index 394d0606..3bb60c46 100644 --- a/src/linter/reporters/console.mjs +++ b/src/linter/reporters/console.mjs @@ -5,8 +5,7 @@ import { styleText } from 'node:util'; /** - * TODO is there a way to grab the parameter type for styleText since the types aren't exported - * @type {Record} + * @type {Record} */ const levelToColorMap = { info: 'gray', @@ -17,6 +16,12 @@ const levelToColorMap = { /** * @type {import('../types.d.ts').Reporter} */ -export default msg => { - console.log(styleText(levelToColorMap[msg.level], msg.msg)); +export default issue => { + console.log( + styleText( + // @ts-expect-error ForegroundColors is not exported + levelToColorMap[issue.level], + `${issue.message} at ${issue.location.path} (${issue.location.position.start}:${issue.location.position.end})` + ) + ); }; diff --git a/src/linter/reporters/github.mjs b/src/linter/reporters/github.mjs index fdd005ab..0baa4eb8 100644 --- a/src/linter/reporters/github.mjs +++ b/src/linter/reporters/github.mjs @@ -2,12 +2,23 @@ 'use strict'; +import * as core from '@actions/core'; + /** * GitHub action reporter for * * @type {import('../types.d.ts').Reporter} */ -export default msg => { - // TODO - console.log(msg); +export default issue => { + const actions = { + warn: core.warning, + error: core.error, + info: core.notice, + }; + + (actions[issue.level] || core.notice)(issue.message, { + file: issue.location.path, + startLine: issue.location.position.start.line, + endLine: issue.location.position.end.line, + }); }; diff --git a/src/linter/rules/invalid-change-version.mjs b/src/linter/rules/invalid-change-version.mjs new file mode 100644 index 00000000..34e368f0 --- /dev/null +++ b/src/linter/rules/invalid-change-version.mjs @@ -0,0 +1,33 @@ +import { validateVersion } from '../utils/semver.mjs'; + +/** + * Checks if any change version is invalid. + * + * @param {ApiDocMetadataEntry} entry + * @returns {Array} + */ +export const invalidChangeVersion = entry => { + if (entry.changes.length === 0) { + return []; + } + + const allVersions = entry.changes + .filter(change => change.version) + .flatMap(change => + Array.isArray(change.version) ? change.version : [change.version] + ); + + const invalidVersions = allVersions.filter( + version => !validateVersion(version.substring(1)) // Trim the leading 'v' from the version string + ); + + return invalidVersions.map(version => ({ + level: 'warn', + message: `Invalid version number: ${version}`, + location: { + path: entry.api_doc_source, + line: entry.yaml_position.start.line, + column: entry.yaml_position.start.column, + }, + })); +}; diff --git a/src/linter/rules/missing-change-version.mjs b/src/linter/rules/missing-change-version.mjs new file mode 100644 index 00000000..67baaa23 --- /dev/null +++ b/src/linter/rules/missing-change-version.mjs @@ -0,0 +1,23 @@ +/** + * Checks if any change version is missing. + * + * @param {ApiDocMetadataEntry} entry + * @returns {Array} + */ +export const missingChangeVersion = entry => { + if (entry.changes.length === 0) { + return []; + } + + return entry.changes + .filter(change => !change.version) + .map(() => ({ + level: 'warn', + message: 'Missing change version', + location: { + path: entry.api_doc_source, + line: entry.yaml_position.start.line, + column: entry.yaml_position.start.column, + }, + })); +}; diff --git a/src/linter/rules/missing-introduced-in.mjs b/src/linter/rules/missing-introduced-in.mjs new file mode 100644 index 00000000..e2c779db --- /dev/null +++ b/src/linter/rules/missing-introduced-in.mjs @@ -0,0 +1,24 @@ +/** + * Checks if `introduced_in` field is missing in the API doc entry. + * + * @param {ApiDocMetadataEntry} entry + * @returns {Array} + */ +export const missingIntroducedIn = entry => { + // Early return if not a top-level heading or if introduced_in exists + if (entry.heading.depth !== 1 || entry.introduced_in) { + return []; + } + + return [ + { + level: 'info', + message: 'Missing `introduced_in` field in the API doc entry', + location: { + path: entry.api_doc_source, + // line: entry.yaml_position.start, + // column: entry.yaml_position.end, + }, + }, + ]; +}; diff --git a/src/linter/types.d.ts b/src/linter/types.d.ts index 886ba023..e30cb1bf 100644 --- a/src/linter/types.d.ts +++ b/src/linter/types.d.ts @@ -1,16 +1,18 @@ -export type LintLevel = 'info' | 'warn' | 'error'; +import { Position } from 'unist'; -export interface LintMessageLocation { - // The absolute path to the file - path: string; - line: number; - column: number; +export type IssueLevel = 'info' | 'warn' | 'error'; + +export interface LintIssueLocation { + path: string; // The absolute path to the file + position: Position; } -export interface LintMessage { - level: LintLevel; - msg: string; - location?: LintMessageLocation; +export interface LintIssue { + level: IssueLevel; + message: string; + location: LintIssueLocation; } -export type Reporter = (msg: LintMessage) => void; +type LintRule = (input: ApiDocMetadataEntry) => LintIssue[]; + +export type Reporter = (msg: LintIssue) => void; diff --git a/src/linter/utils/semver.mjs b/src/linter/utils/semver.mjs new file mode 100644 index 00000000..24294b34 --- /dev/null +++ b/src/linter/utils/semver.mjs @@ -0,0 +1,12 @@ +/** + * Validates a semver version string + * + * @param {string} version + */ +export const validateVersion = version => { + // https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string + const regex = + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/gm; + + return regex.test(version); +}; diff --git a/src/metadata.mjs b/src/metadata.mjs index 579061f1..8c30a80f 100644 --- a/src/metadata.mjs +++ b/src/metadata.mjs @@ -45,6 +45,7 @@ const createMetadata = slugger => { const internalMetadata = { heading: createTree('root', { data: {} }), stability: createTree('root', []), + yaml_position: {}, properties: { type: undefined, changes: [], tags: [] }, }; @@ -92,6 +93,14 @@ const createMetadata = slugger => { internalMetadata.properties[key] = value; }); }, + /** + * Set the YAML position of the current Metadata entry + * + * @param {import('unist').Position} yaml_position + */ + setYamlPosition: yaml_position => { + internalMetadata.yaml_position = yaml_position; + }, /** * Generates a new Navigation entry and pushes them to the internal collection * of Navigation entries, and returns a MetadataEntry which is then used by the parser @@ -159,6 +168,7 @@ const createMetadata = slugger => { content: section, tags, introduced_in, + yaml_position: internalMetadata.yaml_position, }; }, }; diff --git a/src/parsers/markdown.mjs b/src/parsers/markdown.mjs index a0110f88..800a51b6 100644 --- a/src/parsers/markdown.mjs +++ b/src/parsers/markdown.mjs @@ -17,11 +17,11 @@ import { createNodeSlugger } from '../utils/slugger.mjs'; * * @param {import('./linter/index.mjs').Linter | undefined} linter */ -const createParser = linter => { +const createParser = () => { // Creates an instance of the Remark processor with GFM support // which is used for stringifying the AST tree back to Markdown const remarkProcessor = getRemark(); - linter?.info('asd123'); + const { setHeadingMetadata, addYAMLMetadata, @@ -136,9 +136,11 @@ const createParser = linter => { // Visits all HTML nodes from the current subtree and if there's any that matches // our YAML metadata structure, it transforms into YAML metadata // and then apply the YAML Metadata to the current Metadata entry - visit(subTree, createQueries.UNIST.isYamlNode, node => - addYAMLMetadata(node, apiEntryMetadata) - ); + visit(subTree, createQueries.UNIST.isYamlNode, node => { + // TODO: Is there always only one YAML node? + apiEntryMetadata.setYamlPosition(node.position); + addYAMLMetadata(node, apiEntryMetadata); + }); // Visits all Text nodes from the current subtree and if there's any that matches // any API doc type reference and then updates the type reference to be a Markdown link diff --git a/src/types.d.ts b/src/types.d.ts index ebae255e..cb89e40a 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,7 +1,7 @@ import type { Heading, Root } from '@types/mdast'; import type { Program } from 'acorn'; import type { SemVer } from 'semver'; -import type { Data, Node, Parent } from 'unist'; +import type { Data, Node, Parent, Position } from 'unist'; // String serialization of the AST tree // @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#tojson_behavior @@ -99,6 +99,7 @@ declare global { // Extra YAML section entries that are stringd and serve // to provide additional metadata about the API doc entry tags: Array; + yaml_position: Position; } export interface JsProgram extends Program { From 4aa4f774677db099cb9cb55c3a3bf45dcf060512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Mon, 17 Feb 2025 13:21:38 -0300 Subject: [PATCH 03/22] fix: optional location --- src/linter/reporters/console.mjs | 6 +++++- src/linter/types.d.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/linter/reporters/console.mjs b/src/linter/reporters/console.mjs index 3bb60c46..ddad029f 100644 --- a/src/linter/reporters/console.mjs +++ b/src/linter/reporters/console.mjs @@ -17,11 +17,15 @@ const levelToColorMap = { * @type {import('../types.d.ts').Reporter} */ export default issue => { + const position = issue.location.position + ? ` (${issue.location.position.start}:${issue.location.position.end})` + : ''; + console.log( styleText( // @ts-expect-error ForegroundColors is not exported levelToColorMap[issue.level], - `${issue.message} at ${issue.location.path} (${issue.location.position.start}:${issue.location.position.end})` + `${issue.message} at ${issue.location.path}${position}` ) ); }; diff --git a/src/linter/types.d.ts b/src/linter/types.d.ts index e30cb1bf..719b1fd8 100644 --- a/src/linter/types.d.ts +++ b/src/linter/types.d.ts @@ -4,7 +4,7 @@ export type IssueLevel = 'info' | 'warn' | 'error'; export interface LintIssueLocation { path: string; // The absolute path to the file - position: Position; + position?: Position; } export interface LintIssue { From 5e1c0ecde5a6a020f7a095c4e586c2d648435888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Mon, 17 Feb 2025 13:22:49 -0300 Subject: [PATCH 04/22] fix: create hasError getter --- src/linter/index.mjs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/linter/index.mjs b/src/linter/index.mjs index 656203be..616b8b87 100644 --- a/src/linter/index.mjs +++ b/src/linter/index.mjs @@ -54,4 +54,13 @@ export class Linter { reporter(issue); } } + + /** + * Returns whether there are any issues with a level of 'error' + * + * @returns {boolean} + */ + get hasError() { + return this.#issues.some(issue => issue.level === 'error'); + } } From b3964445536c34ef41fefec680910a3df456524e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Mon, 17 Feb 2025 13:25:03 -0300 Subject: [PATCH 05/22] fix: optional location --- src/linter/reporters/github.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/linter/reporters/github.mjs b/src/linter/reporters/github.mjs index 0baa4eb8..36958de8 100644 --- a/src/linter/reporters/github.mjs +++ b/src/linter/reporters/github.mjs @@ -18,7 +18,7 @@ export default issue => { (actions[issue.level] || core.notice)(issue.message, { file: issue.location.path, - startLine: issue.location.position.start.line, - endLine: issue.location.position.end.line, + startLine: issue.location.position?.start.line, + endLine: issue.location.position?.end.line, }); }; From b443a7ac1f60f7ee97f1341feb06936700df2ed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Tue, 18 Feb 2025 16:20:55 -0300 Subject: [PATCH 06/22] test: update expected metadata --- src/test/metadata.test.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/metadata.test.mjs b/src/test/metadata.test.mjs index 96c20819..a36a99e9 100644 --- a/src/test/metadata.test.mjs +++ b/src/test/metadata.test.mjs @@ -79,6 +79,7 @@ describe('createMetadata', () => { stability: { type: 'root', children: [stability] }, tags: [], updates: [], + yaml_position: {}, }; const actual = metadata.create(apiDoc, section); delete actual.stability.toJSON; From 5248f116840aa3d7de3eff49eba4ee88ca65b365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Thu, 20 Feb 2025 11:50:26 -0300 Subject: [PATCH 07/22] refactor: some changes --- bin/cli.mjs | 6 +- docs/assert.md | 2686 ++++++++++++++++++++++++++++++ src/linter/index.mjs | 1 - src/linter/reporters/console.mjs | 2 - src/linter/reporters/github.mjs | 16 +- 5 files changed, 2698 insertions(+), 13 deletions(-) create mode 100644 docs/assert.md diff --git a/bin/cli.mjs b/bin/cli.mjs index 3325a2ce..f64d2735 100755 --- a/bin/cli.mjs +++ b/bin/cli.mjs @@ -69,6 +69,8 @@ program * @property {Target[]} target Specifies the generator target mode. * @property {string} version Specifies the target Node.js version. * @property {string} changelog Specifies the path to the Node.js CHANGELOG.md file + * @property {boolean} skipLinting Specifies whether to skip linting + * @property {keyof reporters} reporter Specifies the linter reporter * * @name ProgramOptions * @type {Options} @@ -80,11 +82,11 @@ const { target = [], version, changelog, - skipValidation, + skipLinting, reporter, } = program.opts(); -const linter = skipValidation ? undefined : new Linter(); +const linter = skipLinting ? undefined : new Linter(); const { loadFiles } = createMarkdownLoader(); const { parseApiDocs } = createMarkdownParser(); diff --git a/docs/assert.md b/docs/assert.md new file mode 100644 index 00000000..2b61b5b5 --- /dev/null +++ b/docs/assert.md @@ -0,0 +1,2686 @@ +# Assert + +> Stability: 2 - Stable + + + +The `node:assert` module provides a set of assertion functions for verifying +invariants. + +## Strict assertion mode + + + +In strict assertion mode, non-strict methods behave like their corresponding +strict methods. For example, [`assert.deepEqual()`][] will behave like +[`assert.deepStrictEqual()`][]. + +In strict assertion mode, error messages for objects display a diff. In legacy +assertion mode, error messages for objects display the objects, often truncated. + +To use strict assertion mode: + +```mjs +import { strict as assert } from 'node:assert'; +``` + +```cjs +const assert = require('node:assert').strict; +``` + +```mjs +import assert from 'node:assert/strict'; +``` + +```cjs +const assert = require('node:assert/strict'); +``` + +Example error diff: + +```mjs +import { strict as assert } from 'node:assert'; + +assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]); +// AssertionError: Expected inputs to be strictly deep-equal: +// + actual - expected ... Lines skipped +// +// [ +// [ +// ... +// 2, +// + 3 +// - '3' +// ], +// ... +// 5 +// ] +``` + +```cjs +const assert = require('node:assert/strict'); + +assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]); +// AssertionError: Expected inputs to be strictly deep-equal: +// + actual - expected ... Lines skipped +// +// [ +// [ +// ... +// 2, +// + 3 +// - '3' +// ], +// ... +// 5 +// ] +``` + +To deactivate the colors, use the `NO_COLOR` or `NODE_DISABLE_COLORS` +environment variables. This will also deactivate the colors in the REPL. For +more on color support in terminal environments, read the tty +[`getColorDepth()`][] documentation. + +## Legacy assertion mode + +Legacy assertion mode uses the [`==` operator][] in: + +- [`assert.deepEqual()`][] +- [`assert.equal()`][] +- [`assert.notDeepEqual()`][] +- [`assert.notEqual()`][] + +To use legacy assertion mode: + +```mjs +import assert from 'node:assert'; +``` + +```cjs +const assert = require('node:assert'); +``` + +Legacy assertion mode may have surprising results, especially when using +[`assert.deepEqual()`][]: + +```cjs +// WARNING: This does not throw an AssertionError in legacy assertion mode! +assert.deepEqual(/a/gi, new Date()); +``` + +## Class: assert.AssertionError + +- Extends: {errors.Error} + +Indicates the failure of an assertion. All errors thrown by the `node:assert` +module will be instances of the `AssertionError` class. + +### `new assert.AssertionError(options)` + + + +- `options` {Object} + - `message` {string} If provided, the error message is set to this value. + - `actual` {any} The `actual` property on the error instance. + - `expected` {any} The `expected` property on the error instance. + - `operator` {string} The `operator` property on the error instance. + - `stackStartFn` {Function} If provided, the generated stack trace omits + frames before this function. + +A subclass of {Error} that indicates the failure of an assertion. + +All instances contain the built-in `Error` properties (`message` and `name`) +and: + +- `actual` {any} Set to the `actual` argument for methods such as + [`assert.strictEqual()`][]. +- `expected` {any} Set to the `expected` value for methods such as + [`assert.strictEqual()`][]. +- `generatedMessage` {boolean} Indicates if the message was auto-generated + (`true`) or not. +- `code` {string} Value is always `ERR_ASSERTION` to show that the error is an + assertion error. +- `operator` {string} Set to the passed in operator value. + +```mjs +import assert from 'node:assert'; + +// Generate an AssertionError to compare the error message later: +const { message } = new assert.AssertionError({ + actual: 1, + expected: 2, + operator: 'strictEqual', +}); + +// Verify error output: +try { + assert.strictEqual(1, 2); +} catch (err) { + assert(err instanceof assert.AssertionError); + assert.strictEqual(err.message, message); + assert.strictEqual(err.name, 'AssertionError'); + assert.strictEqual(err.actual, 1); + assert.strictEqual(err.expected, 2); + assert.strictEqual(err.code, 'ERR_ASSERTION'); + assert.strictEqual(err.operator, 'strictEqual'); + assert.strictEqual(err.generatedMessage, true); +} +``` + +```cjs +const assert = require('node:assert'); + +// Generate an AssertionError to compare the error message later: +const { message } = new assert.AssertionError({ + actual: 1, + expected: 2, + operator: 'strictEqual', +}); + +// Verify error output: +try { + assert.strictEqual(1, 2); +} catch (err) { + assert(err instanceof assert.AssertionError); + assert.strictEqual(err.message, message); + assert.strictEqual(err.name, 'AssertionError'); + assert.strictEqual(err.actual, 1); + assert.strictEqual(err.expected, 2); + assert.strictEqual(err.code, 'ERR_ASSERTION'); + assert.strictEqual(err.operator, 'strictEqual'); + assert.strictEqual(err.generatedMessage, true); +} +``` + +## Class: `assert.CallTracker` + + + +> Stability: 0 - Deprecated + +This feature is deprecated and will be removed in a future version. +Please consider using alternatives such as the +[`mock`][] helper function. + +### `new assert.CallTracker()` + + + +Creates a new [`CallTracker`][] object which can be used to track if functions +were called a specific number of times. The `tracker.verify()` must be called +for the verification to take place. The usual pattern would be to call it in a +[`process.on('exit')`][] handler. + +```mjs +import assert from 'node:assert'; +import process from 'node:process'; + +const tracker = new assert.CallTracker(); + +function func() {} + +// callsfunc() must be called exactly 1 time before tracker.verify(). +const callsfunc = tracker.calls(func, 1); + +callsfunc(); + +// Calls tracker.verify() and verifies if all tracker.calls() functions have +// been called exact times. +process.on('exit', () => { + tracker.verify(); +}); +``` + +```cjs +const assert = require('node:assert'); +const process = require('node:process'); + +const tracker = new assert.CallTracker(); + +function func() {} + +// callsfunc() must be called exactly 1 time before tracker.verify(). +const callsfunc = tracker.calls(func, 1); + +callsfunc(); + +// Calls tracker.verify() and verifies if all tracker.calls() functions have +// been called exact times. +process.on('exit', () => { + tracker.verify(); +}); +``` + +### `tracker.calls([fn][, exact])` + + + +- `fn` {Function} **Default:** A no-op function. +- `exact` {number} **Default:** `1`. +- Returns: {Function} A function that wraps `fn`. + +The wrapper function is expected to be called exactly `exact` times. If the +function has not been called exactly `exact` times when +[`tracker.verify()`][] is called, then [`tracker.verify()`][] will throw an +error. + +```mjs +import assert from 'node:assert'; + +// Creates call tracker. +const tracker = new assert.CallTracker(); + +function func() {} + +// Returns a function that wraps func() that must be called exact times +// before tracker.verify(). +const callsfunc = tracker.calls(func); +``` + +```cjs +const assert = require('node:assert'); + +// Creates call tracker. +const tracker = new assert.CallTracker(); + +function func() {} + +// Returns a function that wraps func() that must be called exact times +// before tracker.verify(). +const callsfunc = tracker.calls(func); +``` + +### `tracker.getCalls(fn)` + + + +- `fn` {Function} + +- Returns: {Array} An array with all the calls to a tracked function. + +- Object {Object} + - `thisArg` {Object} + - `arguments` {Array} the arguments passed to the tracked function + +```mjs +import assert from 'node:assert'; + +const tracker = new assert.CallTracker(); + +function func() {} +const callsfunc = tracker.calls(func); +callsfunc(1, 2, 3); + +assert.deepStrictEqual(tracker.getCalls(callsfunc), [ + { thisArg: undefined, arguments: [1, 2, 3] }, +]); +``` + +```cjs +const assert = require('node:assert'); + +// Creates call tracker. +const tracker = new assert.CallTracker(); + +function func() {} +const callsfunc = tracker.calls(func); +callsfunc(1, 2, 3); + +assert.deepStrictEqual(tracker.getCalls(callsfunc), [ + { thisArg: undefined, arguments: [1, 2, 3] }, +]); +``` + +### `tracker.report()` + + + +- Returns: {Array} An array of objects containing information about the wrapper + functions returned by [`tracker.calls()`][]. +- Object {Object} + - `message` {string} + - `actual` {number} The actual number of times the function was called. + - `expected` {number} The number of times the function was expected to be + called. + - `operator` {string} The name of the function that is wrapped. + - `stack` {Object} A stack trace of the function. + +The arrays contains information about the expected and actual number of calls of +the functions that have not been called the expected number of times. + +```mjs +import assert from 'node:assert'; + +// Creates call tracker. +const tracker = new assert.CallTracker(); + +function func() {} + +// Returns a function that wraps func() that must be called exact times +// before tracker.verify(). +const callsfunc = tracker.calls(func, 2); + +// Returns an array containing information on callsfunc() +console.log(tracker.report()); +// [ +// { +// message: 'Expected the func function to be executed 2 time(s) but was +// executed 0 time(s).', +// actual: 0, +// expected: 2, +// operator: 'func', +// stack: stack trace +// } +// ] +``` + +```cjs +const assert = require('node:assert'); + +// Creates call tracker. +const tracker = new assert.CallTracker(); + +function func() {} + +// Returns a function that wraps func() that must be called exact times +// before tracker.verify(). +const callsfunc = tracker.calls(func, 2); + +// Returns an array containing information on callsfunc() +console.log(tracker.report()); +// [ +// { +// message: 'Expected the func function to be executed 2 time(s) but was +// executed 0 time(s).', +// actual: 0, +// expected: 2, +// operator: 'func', +// stack: stack trace +// } +// ] +``` + +### `tracker.reset([fn])` + + + +- `fn` {Function} a tracked function to reset. + +Reset calls of the call tracker. +If a tracked function is passed as an argument, the calls will be reset for it. +If no arguments are passed, all tracked functions will be reset. + +```mjs +import assert from 'node:assert'; + +const tracker = new assert.CallTracker(); + +function func() {} +const callsfunc = tracker.calls(func); + +callsfunc(); +// Tracker was called once +assert.strictEqual(tracker.getCalls(callsfunc).length, 1); + +tracker.reset(callsfunc); +assert.strictEqual(tracker.getCalls(callsfunc).length, 0); +``` + +```cjs +const assert = require('node:assert'); + +const tracker = new assert.CallTracker(); + +function func() {} +const callsfunc = tracker.calls(func); + +callsfunc(); +// Tracker was called once +assert.strictEqual(tracker.getCalls(callsfunc).length, 1); + +tracker.reset(callsfunc); +assert.strictEqual(tracker.getCalls(callsfunc).length, 0); +``` + +### `tracker.verify()` + + + +Iterates through the list of functions passed to +[`tracker.calls()`][] and will throw an error for functions that +have not been called the expected number of times. + +```mjs +import assert from 'node:assert'; + +// Creates call tracker. +const tracker = new assert.CallTracker(); + +function func() {} + +// Returns a function that wraps func() that must be called exact times +// before tracker.verify(). +const callsfunc = tracker.calls(func, 2); + +callsfunc(); + +// Will throw an error since callsfunc() was only called once. +tracker.verify(); +``` + +```cjs +const assert = require('node:assert'); + +// Creates call tracker. +const tracker = new assert.CallTracker(); + +function func() {} + +// Returns a function that wraps func() that must be called exact times +// before tracker.verify(). +const callsfunc = tracker.calls(func, 2); + +callsfunc(); + +// Will throw an error since callsfunc() was only called once. +tracker.verify(); +``` + +## `assert(value[, message])` + + + +- `value` {any} The input that is checked for being truthy. +- `message` {string|Error} + +An alias of [`assert.ok()`][]. + +## `assert.deepEqual(actual, expected[, message])` + + + +- `actual` {any} +- `expected` {any} +- `message` {string|Error} + +**Strict assertion mode** + +An alias of [`assert.deepStrictEqual()`][]. + +**Legacy assertion mode** + +> Stability: 3 - Legacy: Use [`assert.deepStrictEqual()`][] instead. + +Tests for deep equality between the `actual` and `expected` parameters. Consider +using [`assert.deepStrictEqual()`][] instead. [`assert.deepEqual()`][] can have +surprising results. + +_Deep equality_ means that the enumerable "own" properties of child objects +are also recursively evaluated by the following rules. + +### Comparison details + +- Primitive values are compared with the [`==` operator][], + with the exception of {NaN}. It is treated as being identical in case + both sides are {NaN}. +- [Type tags][Object.prototype.toString()] of objects should be the same. +- Only [enumerable "own" properties][] are considered. +- {Error} names, messages, causes, and errors are always compared, + even if these are not enumerable properties. +- [Object wrappers][] are compared both as objects and unwrapped values. +- `Object` properties are compared unordered. +- {Map} keys and {Set} items are compared unordered. +- Recursion stops when both sides differ or both sides encounter a circular + reference. +- Implementation does not test the [`[[Prototype]]`][prototype-spec] of + objects. +- {Symbol} properties are not compared. +- {WeakMap} and {WeakSet} comparison does not rely on their values + but only on their instances. +- {RegExp} lastIndex, flags, and source are always compared, even if these + are not enumerable properties. + +The following example does not throw an [`AssertionError`][] because the +primitives are compared using the [`==` operator][]. + +```mjs +import assert from 'node:assert'; +// WARNING: This does not throw an AssertionError! + +assert.deepEqual('+00000000', false); +``` + +```cjs +const assert = require('node:assert'); +// WARNING: This does not throw an AssertionError! + +assert.deepEqual('+00000000', false); +``` + +"Deep" equality means that the enumerable "own" properties of child objects +are evaluated also: + +```mjs +import assert from 'node:assert'; + +const obj1 = { + a: { + b: 1, + }, +}; +const obj2 = { + a: { + b: 2, + }, +}; +const obj3 = { + a: { + b: 1, + }, +}; +const obj4 = { __proto__: obj1 }; + +assert.deepEqual(obj1, obj1); +// OK + +// Values of b are different: +assert.deepEqual(obj1, obj2); +// AssertionError: { a: { b: 1 } } deepEqual { a: { b: 2 } } + +assert.deepEqual(obj1, obj3); +// OK + +// Prototypes are ignored: +assert.deepEqual(obj1, obj4); +// AssertionError: { a: { b: 1 } } deepEqual {} +``` + +```cjs +const assert = require('node:assert'); + +const obj1 = { + a: { + b: 1, + }, +}; +const obj2 = { + a: { + b: 2, + }, +}; +const obj3 = { + a: { + b: 1, + }, +}; +const obj4 = { __proto__: obj1 }; + +assert.deepEqual(obj1, obj1); +// OK + +// Values of b are different: +assert.deepEqual(obj1, obj2); +// AssertionError: { a: { b: 1 } } deepEqual { a: { b: 2 } } + +assert.deepEqual(obj1, obj3); +// OK + +// Prototypes are ignored: +assert.deepEqual(obj1, obj4); +// AssertionError: { a: { b: 1 } } deepEqual {} +``` + +If the values are not equal, an [`AssertionError`][] is thrown with a `message` +property set equal to the value of the `message` parameter. If the `message` +parameter is undefined, a default error message is assigned. If the `message` +parameter is an instance of {Error} then it will be thrown instead of the +[`AssertionError`][]. + +## `assert.deepStrictEqual(actual, expected[, message])` + + + +- `actual` {any} +- `expected` {any} +- `message` {string|Error} + +Tests for deep equality between the `actual` and `expected` parameters. +"Deep" equality means that the enumerable "own" properties of child objects +are recursively evaluated also by the following rules. + +### Comparison details + +- Primitive values are compared using [`Object.is()`][]. +- [Type tags][Object.prototype.toString()] of objects should be the same. +- [`[[Prototype]]`][prototype-spec] of objects are compared using + the [`===` operator][]. +- Only [enumerable "own" properties][] are considered. +- {Error} names, messages, causes, and errors are always compared, + even if these are not enumerable properties. + `errors` is also compared. +- Enumerable own {Symbol} properties are compared as well. +- [Object wrappers][] are compared both as objects and unwrapped values. +- `Object` properties are compared unordered. +- {Map} keys and {Set} items are compared unordered. +- Recursion stops when both sides differ or both sides encounter a circular + reference. +- {WeakMap} and {WeakSet} instances are **not** compared structurally. + They are only equal if they reference the same object. Any comparison between + different `WeakMap` or `WeakSet` instances will result in inequality, + even if they contain the same entries. +- {RegExp} lastIndex, flags, and source are always compared, even if these + are not enumerable properties. + +```mjs +import assert from 'node:assert/strict'; + +// This fails because 1 !== '1'. +assert.deepStrictEqual({ a: 1 }, { a: '1' }); +// AssertionError: Expected inputs to be strictly deep-equal: +// + actual - expected +// +// { +// + a: 1 +// - a: '1' +// } + +// The following objects don't have own properties +const date = new Date(); +const object = {}; +const fakeDate = {}; +Object.setPrototypeOf(fakeDate, Date.prototype); + +// Different [[Prototype]]: +assert.deepStrictEqual(object, fakeDate); +// AssertionError: Expected inputs to be strictly deep-equal: +// + actual - expected +// +// + {} +// - Date {} + +// Different type tags: +assert.deepStrictEqual(date, fakeDate); +// AssertionError: Expected inputs to be strictly deep-equal: +// + actual - expected +// +// + 2018-04-26T00:49:08.604Z +// - Date {} + +assert.deepStrictEqual(NaN, NaN); +// OK because Object.is(NaN, NaN) is true. + +// Different unwrapped numbers: +assert.deepStrictEqual(new Number(1), new Number(2)); +// AssertionError: Expected inputs to be strictly deep-equal: +// + actual - expected +// +// + [Number: 1] +// - [Number: 2] + +assert.deepStrictEqual(new String('foo'), Object('foo')); +// OK because the object and the string are identical when unwrapped. + +assert.deepStrictEqual(-0, -0); +// OK + +// Different zeros: +assert.deepStrictEqual(0, -0); +// AssertionError: Expected inputs to be strictly deep-equal: +// + actual - expected +// +// + 0 +// - -0 + +const symbol1 = Symbol(); +const symbol2 = Symbol(); +assert.deepStrictEqual({ [symbol1]: 1 }, { [symbol1]: 1 }); +// OK, because it is the same symbol on both objects. + +assert.deepStrictEqual({ [symbol1]: 1 }, { [symbol2]: 1 }); +// AssertionError [ERR_ASSERTION]: Inputs identical but not reference equal: +// +// { +// Symbol(): 1 +// } + +const weakMap1 = new WeakMap(); +const weakMap2 = new WeakMap(); +const obj = {}; + +weakMap1.set(obj, 'value'); +weakMap2.set(obj, 'value'); + +// Comparing different instances fails, even with same contents +assert.deepStrictEqual(weakMap1, weakMap2); +// AssertionError: Values have same structure but are not reference-equal: +// +// WeakMap { +// +// } + +// Comparing the same instance to itself succeeds +assert.deepStrictEqual(weakMap1, weakMap1); +// OK + +const weakSet1 = new WeakSet(); +const weakSet2 = new WeakSet(); +weakSet1.add(obj); +weakSet2.add(obj); + +// Comparing different instances fails, even with same contents +assert.deepStrictEqual(weakSet1, weakSet2); +// AssertionError: Values have same structure but are not reference-equal: +// + actual - expected +// +// WeakSet { +// +// } + +// Comparing the same instance to itself succeeds +assert.deepStrictEqual(weakSet1, weakSet1); +// OK +``` + +```cjs +const assert = require('node:assert/strict'); + +// This fails because 1 !== '1'. +assert.deepStrictEqual({ a: 1 }, { a: '1' }); +// AssertionError: Expected inputs to be strictly deep-equal: +// + actual - expected +// +// { +// + a: 1 +// - a: '1' +// } + +// The following objects don't have own properties +const date = new Date(); +const object = {}; +const fakeDate = {}; +Object.setPrototypeOf(fakeDate, Date.prototype); + +// Different [[Prototype]]: +assert.deepStrictEqual(object, fakeDate); +// AssertionError: Expected inputs to be strictly deep-equal: +// + actual - expected +// +// + {} +// - Date {} + +// Different type tags: +assert.deepStrictEqual(date, fakeDate); +// AssertionError: Expected inputs to be strictly deep-equal: +// + actual - expected +// +// + 2018-04-26T00:49:08.604Z +// - Date {} + +assert.deepStrictEqual(NaN, NaN); +// OK because Object.is(NaN, NaN) is true. + +// Different unwrapped numbers: +assert.deepStrictEqual(new Number(1), new Number(2)); +// AssertionError: Expected inputs to be strictly deep-equal: +// + actual - expected +// +// + [Number: 1] +// - [Number: 2] + +assert.deepStrictEqual(new String('foo'), Object('foo')); +// OK because the object and the string are identical when unwrapped. + +assert.deepStrictEqual(-0, -0); +// OK + +// Different zeros: +assert.deepStrictEqual(0, -0); +// AssertionError: Expected inputs to be strictly deep-equal: +// + actual - expected +// +// + 0 +// - -0 + +const symbol1 = Symbol(); +const symbol2 = Symbol(); +assert.deepStrictEqual({ [symbol1]: 1 }, { [symbol1]: 1 }); +// OK, because it is the same symbol on both objects. + +assert.deepStrictEqual({ [symbol1]: 1 }, { [symbol2]: 1 }); +// AssertionError [ERR_ASSERTION]: Inputs identical but not reference equal: +// +// { +// Symbol(): 1 +// } + +const weakMap1 = new WeakMap(); +const weakMap2 = new WeakMap(); +const obj = {}; + +weakMap1.set(obj, 'value'); +weakMap2.set(obj, 'value'); + +// Comparing different instances fails, even with same contents +assert.deepStrictEqual(weakMap1, weakMap2); +// AssertionError: Values have same structure but are not reference-equal: +// +// WeakMap { +// +// } + +// Comparing the same instance to itself succeeds +assert.deepStrictEqual(weakMap1, weakMap1); +// OK + +const weakSet1 = new WeakSet(); +const weakSet2 = new WeakSet(); +weakSet1.add(obj); +weakSet2.add(obj); + +// Comparing different instances fails, even with same contents +assert.deepStrictEqual(weakSet1, weakSet2); +// AssertionError: Values have same structure but are not reference-equal: +// + actual - expected +// +// WeakSet { +// +// } + +// Comparing the same instance to itself succeeds +assert.deepStrictEqual(weakSet1, weakSet1); +// OK +``` + +If the values are not equal, an [`AssertionError`][] is thrown with a `message` +property set equal to the value of the `message` parameter. If the `message` +parameter is undefined, a default error message is assigned. If the `message` +parameter is an instance of {Error} then it will be thrown instead of the +`AssertionError`. + +## `assert.doesNotMatch(string, regexp[, message])` + + + +- `string` {string} +- `regexp` {RegExp} +- `message` {string|Error} + +Expects the `string` input not to match the regular expression. + +```mjs +import assert from 'node:assert/strict'; + +assert.doesNotMatch('I will fail', /fail/); +// AssertionError [ERR_ASSERTION]: The input was expected to not match the ... + +assert.doesNotMatch(123, /pass/); +// AssertionError [ERR_ASSERTION]: The "string" argument must be of type string. + +assert.doesNotMatch('I will pass', /different/); +// OK +``` + +```cjs +const assert = require('node:assert/strict'); + +assert.doesNotMatch('I will fail', /fail/); +// AssertionError [ERR_ASSERTION]: The input was expected to not match the ... + +assert.doesNotMatch(123, /pass/); +// AssertionError [ERR_ASSERTION]: The "string" argument must be of type string. + +assert.doesNotMatch('I will pass', /different/); +// OK +``` + +If the values do match, or if the `string` argument is of another type than +`string`, an [`AssertionError`][] is thrown with a `message` property set equal +to the value of the `message` parameter. If the `message` parameter is +undefined, a default error message is assigned. If the `message` parameter is an +instance of {Error} then it will be thrown instead of the +[`AssertionError`][]. + +## `assert.doesNotReject(asyncFn[, error][, message])` + + + +- `asyncFn` {Function|Promise} +- `error` {RegExp|Function} +- `message` {string} + +Awaits the `asyncFn` promise or, if `asyncFn` is a function, immediately +calls the function and awaits the returned promise to complete. It will then +check that the promise is not rejected. + +If `asyncFn` is a function and it throws an error synchronously, +`assert.doesNotReject()` will return a rejected `Promise` with that error. If +the function does not return a promise, `assert.doesNotReject()` will return a +rejected `Promise` with an [`ERR_INVALID_RETURN_VALUE`][] error. In both cases +the error handler is skipped. + +Using `assert.doesNotReject()` is actually not useful because there is little +benefit in catching a rejection and then rejecting it again. Instead, consider +adding a comment next to the specific code path that should not reject and keep +error messages as expressive as possible. + +If specified, `error` can be a [`Class`][], {RegExp} or a validation +function. See [`assert.throws()`][] for more details. + +Besides the async nature to await the completion behaves identically to +[`assert.doesNotThrow()`][]. + +```mjs +import assert from 'node:assert/strict'; + +await assert.doesNotReject(async () => { + throw new TypeError('Wrong value'); +}, SyntaxError); +``` + +```cjs +const assert = require('node:assert/strict'); + +(async () => { + await assert.doesNotReject(async () => { + throw new TypeError('Wrong value'); + }, SyntaxError); +})(); +``` + +```mjs +import assert from 'node:assert/strict'; + +assert.doesNotReject(Promise.reject(new TypeError('Wrong value'))).then(() => { + // ... +}); +``` + +```cjs +const assert = require('node:assert/strict'); + +assert.doesNotReject(Promise.reject(new TypeError('Wrong value'))).then(() => { + // ... +}); +``` + +## `assert.doesNotThrow(fn[, error][, message])` + + + +- `fn` {Function} +- `error` {RegExp|Function} +- `message` {string} + +Asserts that the function `fn` does not throw an error. + +Using `assert.doesNotThrow()` is actually not useful because there +is no benefit in catching an error and then rethrowing it. Instead, consider +adding a comment next to the specific code path that should not throw and keep +error messages as expressive as possible. + +When `assert.doesNotThrow()` is called, it will immediately call the `fn` +function. + +If an error is thrown and it is the same type as that specified by the `error` +parameter, then an [`AssertionError`][] is thrown. If the error is of a +different type, or if the `error` parameter is undefined, the error is +propagated back to the caller. + +If specified, `error` can be a [`Class`][], {RegExp}, or a validation +function. See [`assert.throws()`][] for more details. + +The following, for instance, will throw the {TypeError} because there is no +matching error type in the assertion: + +```mjs +import assert from 'node:assert/strict'; + +assert.doesNotThrow(() => { + throw new TypeError('Wrong value'); +}, SyntaxError); +``` + +```cjs +const assert = require('node:assert/strict'); + +assert.doesNotThrow(() => { + throw new TypeError('Wrong value'); +}, SyntaxError); +``` + +However, the following will result in an [`AssertionError`][] with the message +'Got unwanted exception...': + +```mjs +import assert from 'node:assert/strict'; + +assert.doesNotThrow(() => { + throw new TypeError('Wrong value'); +}, TypeError); +``` + +```cjs +const assert = require('node:assert/strict'); + +assert.doesNotThrow(() => { + throw new TypeError('Wrong value'); +}, TypeError); +``` + +If an [`AssertionError`][] is thrown and a value is provided for the `message` +parameter, the value of `message` will be appended to the [`AssertionError`][] +message: + +```mjs +import assert from 'node:assert/strict'; + +assert.doesNotThrow( + () => { + throw new TypeError('Wrong value'); + }, + /Wrong value/, + 'Whoops' +); +// Throws: AssertionError: Got unwanted exception: Whoops +``` + +```cjs +const assert = require('node:assert/strict'); + +assert.doesNotThrow( + () => { + throw new TypeError('Wrong value'); + }, + /Wrong value/, + 'Whoops' +); +// Throws: AssertionError: Got unwanted exception: Whoops +``` + +## `assert.equal(actual, expected[, message])` + + + +- `actual` {any} +- `expected` {any} +- `message` {string|Error} + +**Strict assertion mode** + +An alias of [`assert.strictEqual()`][]. + +**Legacy assertion mode** + +> Stability: 3 - Legacy: Use [`assert.strictEqual()`][] instead. + +Tests shallow, coercive equality between the `actual` and `expected` parameters +using the [`==` operator][]. `NaN` is specially handled +and treated as being identical if both sides are `NaN`. + +```mjs +import assert from 'node:assert'; + +assert.equal(1, 1); +// OK, 1 == 1 +assert.equal(1, '1'); +// OK, 1 == '1' +assert.equal(NaN, NaN); +// OK + +assert.equal(1, 2); +// AssertionError: 1 == 2 +assert.equal({ a: { b: 1 } }, { a: { b: 1 } }); +// AssertionError: { a: { b: 1 } } == { a: { b: 1 } } +``` + +```cjs +const assert = require('node:assert'); + +assert.equal(1, 1); +// OK, 1 == 1 +assert.equal(1, '1'); +// OK, 1 == '1' +assert.equal(NaN, NaN); +// OK + +assert.equal(1, 2); +// AssertionError: 1 == 2 +assert.equal({ a: { b: 1 } }, { a: { b: 1 } }); +// AssertionError: { a: { b: 1 } } == { a: { b: 1 } } +``` + +If the values are not equal, an [`AssertionError`][] is thrown with a `message` +property set equal to the value of the `message` parameter. If the `message` +parameter is undefined, a default error message is assigned. If the `message` +parameter is an instance of {Error} then it will be thrown instead of the +`AssertionError`. + +## `assert.fail([message])` + + + +- `message` {string|Error} **Default:** `'Failed'` + +Throws an [`AssertionError`][] with the provided error message or a default +error message. If the `message` parameter is an instance of {Error} then +it will be thrown instead of the [`AssertionError`][]. + +```mjs +import assert from 'node:assert/strict'; + +assert.fail(); +// AssertionError [ERR_ASSERTION]: Failed + +assert.fail('boom'); +// AssertionError [ERR_ASSERTION]: boom + +assert.fail(new TypeError('need array')); +// TypeError: need array +``` + +```cjs +const assert = require('node:assert/strict'); + +assert.fail(); +// AssertionError [ERR_ASSERTION]: Failed + +assert.fail('boom'); +// AssertionError [ERR_ASSERTION]: boom + +assert.fail(new TypeError('need array')); +// TypeError: need array +``` + +Using `assert.fail()` with more than two arguments is possible but deprecated. +See below for further details. + +## `assert.fail(actual, expected[, message[, operator[, stackStartFn]]])` + + + +> Stability: 0 - Deprecated: Use `assert.fail([message])` or other assert +> functions instead. + +- `actual` {any} +- `expected` {any} +- `message` {string|Error} +- `operator` {string} **Default:** `'!='` +- `stackStartFn` {Function} **Default:** `assert.fail` + +If `message` is falsy, the error message is set as the values of `actual` and +`expected` separated by the provided `operator`. If just the two `actual` and +`expected` arguments are provided, `operator` will default to `'!='`. If +`message` is provided as third argument it will be used as the error message and +the other arguments will be stored as properties on the thrown object. If +`stackStartFn` is provided, all stack frames above that function will be +removed from stacktrace (see [`Error.captureStackTrace`][]). If no arguments are +given, the default message `Failed` will be used. + +```mjs +import assert from 'node:assert/strict'; + +assert.fail('a', 'b'); +// AssertionError [ERR_ASSERTION]: 'a' != 'b' + +assert.fail(1, 2, undefined, '>'); +// AssertionError [ERR_ASSERTION]: 1 > 2 + +assert.fail(1, 2, 'fail'); +// AssertionError [ERR_ASSERTION]: fail + +assert.fail(1, 2, 'whoops', '>'); +// AssertionError [ERR_ASSERTION]: whoops + +assert.fail(1, 2, new TypeError('need array')); +// TypeError: need array +``` + +```cjs +const assert = require('node:assert/strict'); + +assert.fail('a', 'b'); +// AssertionError [ERR_ASSERTION]: 'a' != 'b' + +assert.fail(1, 2, undefined, '>'); +// AssertionError [ERR_ASSERTION]: 1 > 2 + +assert.fail(1, 2, 'fail'); +// AssertionError [ERR_ASSERTION]: fail + +assert.fail(1, 2, 'whoops', '>'); +// AssertionError [ERR_ASSERTION]: whoops + +assert.fail(1, 2, new TypeError('need array')); +// TypeError: need array +``` + +In the last three cases `actual`, `expected`, and `operator` have no +influence on the error message. + +Example use of `stackStartFn` for truncating the exception's stacktrace: + +```mjs +import assert from 'node:assert/strict'; + +function suppressFrame() { + assert.fail('a', 'b', undefined, '!==', suppressFrame); +} +suppressFrame(); +// AssertionError [ERR_ASSERTION]: 'a' !== 'b' +// at repl:1:1 +// at ContextifyScript.Script.runInThisContext (vm.js:44:33) +// ... +``` + +```cjs +const assert = require('node:assert/strict'); + +function suppressFrame() { + assert.fail('a', 'b', undefined, '!==', suppressFrame); +} +suppressFrame(); +// AssertionError [ERR_ASSERTION]: 'a' !== 'b' +// at repl:1:1 +// at ContextifyScript.Script.runInThisContext (vm.js:44:33) +// ... +``` + +## `assert.ifError(value)` + + + +- `value` {any} + +Throws `value` if `value` is not `undefined` or `null`. This is useful when +testing the `error` argument in callbacks. The stack trace contains all frames +from the error passed to `ifError()` including the potential new frames for +`ifError()` itself. + +```mjs +import assert from 'node:assert/strict'; + +assert.ifError(null); +// OK +assert.ifError(0); +// AssertionError [ERR_ASSERTION]: ifError got unwanted exception: 0 +assert.ifError('error'); +// AssertionError [ERR_ASSERTION]: ifError got unwanted exception: 'error' +assert.ifError(new Error()); +// AssertionError [ERR_ASSERTION]: ifError got unwanted exception: Error + +// Create some random error frames. +let err; +(function errorFrame() { + err = new Error('test error'); +})(); + +(function ifErrorFrame() { + assert.ifError(err); +})(); +// AssertionError [ERR_ASSERTION]: ifError got unwanted exception: test error +// at ifErrorFrame +// at errorFrame +``` + +```cjs +const assert = require('node:assert/strict'); + +assert.ifError(null); +// OK +assert.ifError(0); +// AssertionError [ERR_ASSERTION]: ifError got unwanted exception: 0 +assert.ifError('error'); +// AssertionError [ERR_ASSERTION]: ifError got unwanted exception: 'error' +assert.ifError(new Error()); +// AssertionError [ERR_ASSERTION]: ifError got unwanted exception: Error + +// Create some random error frames. +let err; +(function errorFrame() { + err = new Error('test error'); +})(); + +(function ifErrorFrame() { + assert.ifError(err); +})(); +// AssertionError [ERR_ASSERTION]: ifError got unwanted exception: test error +// at ifErrorFrame +// at errorFrame +``` + +## `assert.match(string, regexp[, message])` + + + +- `string` {string} +- `regexp` {RegExp} +- `message` {string|Error} + +Expects the `string` input to match the regular expression. + +```mjs +import assert from 'node:assert/strict'; + +assert.match('I will fail', /pass/); +// AssertionError [ERR_ASSERTION]: The input did not match the regular ... + +assert.match(123, /pass/); +// AssertionError [ERR_ASSERTION]: The "string" argument must be of type string. + +assert.match('I will pass', /pass/); +// OK +``` + +```cjs +const assert = require('node:assert/strict'); + +assert.match('I will fail', /pass/); +// AssertionError [ERR_ASSERTION]: The input did not match the regular ... + +assert.match(123, /pass/); +// AssertionError [ERR_ASSERTION]: The "string" argument must be of type string. + +assert.match('I will pass', /pass/); +// OK +``` + +If the values do not match, or if the `string` argument is of another type than +`string`, an [`AssertionError`][] is thrown with a `message` property set equal +to the value of the `message` parameter. If the `message` parameter is +undefined, a default error message is assigned. If the `message` parameter is an +instance of {Error} then it will be thrown instead of the +[`AssertionError`][]. + +## `assert.notDeepEqual(actual, expected[, message])` + + + +- `actual` {any} +- `expected` {any} +- `message` {string|Error} + +**Strict assertion mode** + +An alias of [`assert.notDeepStrictEqual()`][]. + +**Legacy assertion mode** + +> Stability: 3 - Legacy: Use [`assert.notDeepStrictEqual()`][] instead. + +Tests for any deep inequality. Opposite of [`assert.deepEqual()`][]. + +```mjs +import assert from 'node:assert'; + +const obj1 = { + a: { + b: 1, + }, +}; +const obj2 = { + a: { + b: 2, + }, +}; +const obj3 = { + a: { + b: 1, + }, +}; +const obj4 = { __proto__: obj1 }; + +assert.notDeepEqual(obj1, obj1); +// AssertionError: { a: { b: 1 } } notDeepEqual { a: { b: 1 } } + +assert.notDeepEqual(obj1, obj2); +// OK + +assert.notDeepEqual(obj1, obj3); +// AssertionError: { a: { b: 1 } } notDeepEqual { a: { b: 1 } } + +assert.notDeepEqual(obj1, obj4); +// OK +``` + +```cjs +const assert = require('node:assert'); + +const obj1 = { + a: { + b: 1, + }, +}; +const obj2 = { + a: { + b: 2, + }, +}; +const obj3 = { + a: { + b: 1, + }, +}; +const obj4 = { __proto__: obj1 }; + +assert.notDeepEqual(obj1, obj1); +// AssertionError: { a: { b: 1 } } notDeepEqual { a: { b: 1 } } + +assert.notDeepEqual(obj1, obj2); +// OK + +assert.notDeepEqual(obj1, obj3); +// AssertionError: { a: { b: 1 } } notDeepEqual { a: { b: 1 } } + +assert.notDeepEqual(obj1, obj4); +// OK +``` + +If the values are deeply equal, an [`AssertionError`][] is thrown with a +`message` property set equal to the value of the `message` parameter. If the +`message` parameter is undefined, a default error message is assigned. If the +`message` parameter is an instance of {Error} then it will be thrown +instead of the `AssertionError`. + +## `assert.notDeepStrictEqual(actual, expected[, message])` + + + +- `actual` {any} +- `expected` {any} +- `message` {string|Error} + +Tests for deep strict inequality. Opposite of [`assert.deepStrictEqual()`][]. + +```mjs +import assert from 'node:assert/strict'; + +assert.notDeepStrictEqual({ a: 1 }, { a: '1' }); +// OK +``` + +```cjs +const assert = require('node:assert/strict'); + +assert.notDeepStrictEqual({ a: 1 }, { a: '1' }); +// OK +``` + +If the values are deeply and strictly equal, an [`AssertionError`][] is thrown +with a `message` property set equal to the value of the `message` parameter. If +the `message` parameter is undefined, a default error message is assigned. If +the `message` parameter is an instance of {Error} then it will be thrown +instead of the [`AssertionError`][]. + +## `assert.notEqual(actual, expected[, message])` + + + +- `actual` {any} +- `expected` {any} +- `message` {string|Error} + +**Strict assertion mode** + +An alias of [`assert.notStrictEqual()`][]. + +**Legacy assertion mode** + +> Stability: 3 - Legacy: Use [`assert.notStrictEqual()`][] instead. + +Tests shallow, coercive inequality with the [`!=` operator][]. `NaN` is +specially handled and treated as being identical if both sides are `NaN`. + +```mjs +import assert from 'node:assert'; + +assert.notEqual(1, 2); +// OK + +assert.notEqual(1, 1); +// AssertionError: 1 != 1 + +assert.notEqual(1, '1'); +// AssertionError: 1 != '1' +``` + +```cjs +const assert = require('node:assert'); + +assert.notEqual(1, 2); +// OK + +assert.notEqual(1, 1); +// AssertionError: 1 != 1 + +assert.notEqual(1, '1'); +// AssertionError: 1 != '1' +``` + +If the values are equal, an [`AssertionError`][] is thrown with a `message` +property set equal to the value of the `message` parameter. If the `message` +parameter is undefined, a default error message is assigned. If the `message` +parameter is an instance of {Error} then it will be thrown instead of the +`AssertionError`. + +## `assert.notStrictEqual(actual, expected[, message])` + + + +- `actual` {any} +- `expected` {any} +- `message` {string|Error} + +Tests strict inequality between the `actual` and `expected` parameters as +determined by [`Object.is()`][]. + +```mjs +import assert from 'node:assert/strict'; + +assert.notStrictEqual(1, 2); +// OK + +assert.notStrictEqual(1, 1); +// AssertionError [ERR_ASSERTION]: Expected "actual" to be strictly unequal to: +// +// 1 + +assert.notStrictEqual(1, '1'); +// OK +``` + +```cjs +const assert = require('node:assert/strict'); + +assert.notStrictEqual(1, 2); +// OK + +assert.notStrictEqual(1, 1); +// AssertionError [ERR_ASSERTION]: Expected "actual" to be strictly unequal to: +// +// 1 + +assert.notStrictEqual(1, '1'); +// OK +``` + +If the values are strictly equal, an [`AssertionError`][] is thrown with a +`message` property set equal to the value of the `message` parameter. If the +`message` parameter is undefined, a default error message is assigned. If the +`message` parameter is an instance of {Error} then it will be thrown +instead of the `AssertionError`. + +## `assert.ok(value[, message])` + + + +- `value` {any} +- `message` {string|Error} + +Tests if `value` is truthy. It is equivalent to +`assert.equal(!!value, true, message)`. + +If `value` is not truthy, an [`AssertionError`][] is thrown with a `message` +property set equal to the value of the `message` parameter. If the `message` +parameter is `undefined`, a default error message is assigned. If the `message` +parameter is an instance of {Error} then it will be thrown instead of the +`AssertionError`. +If no arguments are passed in at all `message` will be set to the string: +``'No value argument passed to `assert.ok()`'``. + +Be aware that in the `repl` the error message will be different to the one +thrown in a file! See below for further details. + +```mjs +import assert from 'node:assert/strict'; + +assert.ok(true); +// OK +assert.ok(1); +// OK + +assert.ok(); +// AssertionError: No value argument passed to `assert.ok()` + +assert.ok(false, "it's false"); +// AssertionError: it's false + +// In the repl: +assert.ok(typeof 123 === 'string'); +// AssertionError: false == true + +// In a file (e.g. test.js): +assert.ok(typeof 123 === 'string'); +// AssertionError: The expression evaluated to a falsy value: +// +// assert.ok(typeof 123 === 'string') + +assert.ok(false); +// AssertionError: The expression evaluated to a falsy value: +// +// assert.ok(false) + +assert.ok(0); +// AssertionError: The expression evaluated to a falsy value: +// +// assert.ok(0) +``` + +```cjs +const assert = require('node:assert/strict'); + +assert.ok(true); +// OK +assert.ok(1); +// OK + +assert.ok(); +// AssertionError: No value argument passed to `assert.ok()` + +assert.ok(false, "it's false"); +// AssertionError: it's false + +// In the repl: +assert.ok(typeof 123 === 'string'); +// AssertionError: false == true + +// In a file (e.g. test.js): +assert.ok(typeof 123 === 'string'); +// AssertionError: The expression evaluated to a falsy value: +// +// assert.ok(typeof 123 === 'string') + +assert.ok(false); +// AssertionError: The expression evaluated to a falsy value: +// +// assert.ok(false) + +assert.ok(0); +// AssertionError: The expression evaluated to a falsy value: +// +// assert.ok(0) +``` + +```mjs +import assert from 'node:assert/strict'; + +// Using `assert()` works the same: +assert(0); +// AssertionError: The expression evaluated to a falsy value: +// +// assert(0) +``` + +```cjs +const assert = require('node:assert'); + +// Using `assert()` works the same: +assert(0); +// AssertionError: The expression evaluated to a falsy value: +// +// assert(0) +``` + +## `assert.rejects(asyncFn[, error][, message])` + + + +- `asyncFn` {Function|Promise} +- `error` {RegExp|Function|Object|Error} +- `message` {string} + +Awaits the `asyncFn` promise or, if `asyncFn` is a function, immediately +calls the function and awaits the returned promise to complete. It will then +check that the promise is rejected. + +If `asyncFn` is a function and it throws an error synchronously, +`assert.rejects()` will return a rejected `Promise` with that error. If the +function does not return a promise, `assert.rejects()` will return a rejected +`Promise` with an [`ERR_INVALID_RETURN_VALUE`][] error. In both cases the error +handler is skipped. + +Besides the async nature to await the completion behaves identically to +[`assert.throws()`][]. + +If specified, `error` can be a [`Class`][], {RegExp}, a validation function, +an object where each property will be tested for, or an instance of error where +each property will be tested for including the non-enumerable `message` and +`name` properties. + +If specified, `message` will be the message provided by the [`AssertionError`][] +if the `asyncFn` fails to reject. + +```mjs +import assert from 'node:assert/strict'; + +await assert.rejects( + async () => { + throw new TypeError('Wrong value'); + }, + { + name: 'TypeError', + message: 'Wrong value', + } +); +``` + +```cjs +const assert = require('node:assert/strict'); + +(async () => { + await assert.rejects( + async () => { + throw new TypeError('Wrong value'); + }, + { + name: 'TypeError', + message: 'Wrong value', + } + ); +})(); +``` + +```mjs +import assert from 'node:assert/strict'; + +await assert.rejects( + async () => { + throw new TypeError('Wrong value'); + }, + err => { + assert.strictEqual(err.name, 'TypeError'); + assert.strictEqual(err.message, 'Wrong value'); + return true; + } +); +``` + +```cjs +const assert = require('node:assert/strict'); + +(async () => { + await assert.rejects( + async () => { + throw new TypeError('Wrong value'); + }, + err => { + assert.strictEqual(err.name, 'TypeError'); + assert.strictEqual(err.message, 'Wrong value'); + return true; + } + ); +})(); +``` + +```mjs +import assert from 'node:assert/strict'; + +assert.rejects(Promise.reject(new Error('Wrong value')), Error).then(() => { + // ... +}); +``` + +```cjs +const assert = require('node:assert/strict'); + +assert.rejects(Promise.reject(new Error('Wrong value')), Error).then(() => { + // ... +}); +``` + +`error` cannot be a string. If a string is provided as the second +argument, then `error` is assumed to be omitted and the string will be used for +`message` instead. This can lead to easy-to-miss mistakes. Please read the +example in [`assert.throws()`][] carefully if using a string as the second +argument gets considered. + +## `assert.strictEqual(actual, expected[, message])` + + + +- `actual` {any} +- `expected` {any} +- `message` {string|Error} + +Tests strict equality between the `actual` and `expected` parameters as +determined by [`Object.is()`][]. + +```mjs +import assert from 'node:assert/strict'; + +assert.strictEqual(1, 2); +// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal: +// +// 1 !== 2 + +assert.strictEqual(1, 1); +// OK + +assert.strictEqual('Hello foobar', 'Hello World!'); +// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal: +// + actual - expected +// +// + 'Hello foobar' +// - 'Hello World!' +// ^ + +const apples = 1; +const oranges = 2; +assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`); +// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2 + +assert.strictEqual(1, '1', new TypeError('Inputs are not identical')); +// TypeError: Inputs are not identical +``` + +```cjs +const assert = require('node:assert/strict'); + +assert.strictEqual(1, 2); +// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal: +// +// 1 !== 2 + +assert.strictEqual(1, 1); +// OK + +assert.strictEqual('Hello foobar', 'Hello World!'); +// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal: +// + actual - expected +// +// + 'Hello foobar' +// - 'Hello World!' +// ^ + +const apples = 1; +const oranges = 2; +assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`); +// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2 + +assert.strictEqual(1, '1', new TypeError('Inputs are not identical')); +// TypeError: Inputs are not identical +``` + +If the values are not strictly equal, an [`AssertionError`][] is thrown with a +`message` property set equal to the value of the `message` parameter. If the +`message` parameter is undefined, a default error message is assigned. If the +`message` parameter is an instance of {Error} then it will be thrown +instead of the [`AssertionError`][]. + +## `assert.throws(fn[, error][, message])` + + + +- `fn` {Function} +- `error` {RegExp|Function|Object|Error} +- `message` {string} + +Expects the function `fn` to throw an error. + +If specified, `error` can be a [`Class`][], {RegExp}, a validation function, +a validation object where each property will be tested for strict deep equality, +or an instance of error where each property will be tested for strict deep +equality including the non-enumerable `message` and `name` properties. When +using an object, it is also possible to use a regular expression, when +validating against a string property. See below for examples. + +If specified, `message` will be appended to the message provided by the +`AssertionError` if the `fn` call fails to throw or in case the error validation +fails. + +Custom validation object/error instance: + +```mjs +import assert from 'node:assert/strict'; + +const err = new TypeError('Wrong value'); +err.code = 404; +err.foo = 'bar'; +err.info = { + nested: true, + baz: 'text', +}; +err.reg = /abc/i; + +assert.throws( + () => { + throw err; + }, + { + name: 'TypeError', + message: 'Wrong value', + info: { + nested: true, + baz: 'text', + }, + // Only properties on the validation object will be tested for. + // Using nested objects requires all properties to be present. Otherwise + // the validation is going to fail. + } +); + +// Using regular expressions to validate error properties: +assert.throws( + () => { + throw err; + }, + { + // The `name` and `message` properties are strings and using regular + // expressions on those will match against the string. If they fail, an + // error is thrown. + name: /^TypeError$/, + message: /Wrong/, + foo: 'bar', + info: { + nested: true, + // It is not possible to use regular expressions for nested properties! + baz: 'text', + }, + // The `reg` property contains a regular expression and only if the + // validation object contains an identical regular expression, it is going + // to pass. + reg: /abc/i, + } +); + +// Fails due to the different `message` and `name` properties: +assert.throws( + () => { + const otherErr = new Error('Not found'); + // Copy all enumerable properties from `err` to `otherErr`. + for (const [key, value] of Object.entries(err)) { + otherErr[key] = value; + } + throw otherErr; + }, + // The error's `message` and `name` properties will also be checked when using + // an error as validation object. + err +); +``` + +```cjs +const assert = require('node:assert/strict'); + +const err = new TypeError('Wrong value'); +err.code = 404; +err.foo = 'bar'; +err.info = { + nested: true, + baz: 'text', +}; +err.reg = /abc/i; + +assert.throws( + () => { + throw err; + }, + { + name: 'TypeError', + message: 'Wrong value', + info: { + nested: true, + baz: 'text', + }, + // Only properties on the validation object will be tested for. + // Using nested objects requires all properties to be present. Otherwise + // the validation is going to fail. + } +); + +// Using regular expressions to validate error properties: +assert.throws( + () => { + throw err; + }, + { + // The `name` and `message` properties are strings and using regular + // expressions on those will match against the string. If they fail, an + // error is thrown. + name: /^TypeError$/, + message: /Wrong/, + foo: 'bar', + info: { + nested: true, + // It is not possible to use regular expressions for nested properties! + baz: 'text', + }, + // The `reg` property contains a regular expression and only if the + // validation object contains an identical regular expression, it is going + // to pass. + reg: /abc/i, + } +); + +// Fails due to the different `message` and `name` properties: +assert.throws( + () => { + const otherErr = new Error('Not found'); + // Copy all enumerable properties from `err` to `otherErr`. + for (const [key, value] of Object.entries(err)) { + otherErr[key] = value; + } + throw otherErr; + }, + // The error's `message` and `name` properties will also be checked when using + // an error as validation object. + err +); +``` + +Validate instanceof using constructor: + +```mjs +import assert from 'node:assert/strict'; + +assert.throws(() => { + throw new Error('Wrong value'); +}, Error); +``` + +```cjs +const assert = require('node:assert/strict'); + +assert.throws(() => { + throw new Error('Wrong value'); +}, Error); +``` + +Validate error message using {RegExp}: + +Using a regular expression runs `.toString` on the error object, and will +therefore also include the error name. + +```mjs +import assert from 'node:assert/strict'; + +assert.throws(() => { + throw new Error('Wrong value'); +}, /^Error: Wrong value$/); +``` + +```cjs +const assert = require('node:assert/strict'); + +assert.throws(() => { + throw new Error('Wrong value'); +}, /^Error: Wrong value$/); +``` + +Custom error validation: + +The function must return `true` to indicate all internal validations passed. +It will otherwise fail with an [`AssertionError`][]. + +```mjs +import assert from 'node:assert/strict'; + +assert.throws( + () => { + throw new Error('Wrong value'); + }, + err => { + assert(err instanceof Error); + assert(/value/.test(err)); + // Avoid returning anything from validation functions besides `true`. + // Otherwise, it's not clear what part of the validation failed. Instead, + // throw an error about the specific validation that failed (as done in this + // example) and add as much helpful debugging information to that error as + // possible. + return true; + }, + 'unexpected error' +); +``` + +```cjs +const assert = require('node:assert/strict'); + +assert.throws( + () => { + throw new Error('Wrong value'); + }, + err => { + assert(err instanceof Error); + assert(/value/.test(err)); + // Avoid returning anything from validation functions besides `true`. + // Otherwise, it's not clear what part of the validation failed. Instead, + // throw an error about the specific validation that failed (as done in this + // example) and add as much helpful debugging information to that error as + // possible. + return true; + }, + 'unexpected error' +); +``` + +`error` cannot be a string. If a string is provided as the second +argument, then `error` is assumed to be omitted and the string will be used for +`message` instead. This can lead to easy-to-miss mistakes. Using the same +message as the thrown error message is going to result in an +`ERR_AMBIGUOUS_ARGUMENT` error. Please read the example below carefully if using +a string as the second argument gets considered: + +```mjs +import assert from 'node:assert/strict'; + +function throwingFirst() { + throw new Error('First'); +} + +function throwingSecond() { + throw new Error('Second'); +} + +function notThrowing() {} + +// The second argument is a string and the input function threw an Error. +// The first case will not throw as it does not match for the error message +// thrown by the input function! +assert.throws(throwingFirst, 'Second'); +// In the next example the message has no benefit over the message from the +// error and since it is not clear if the user intended to actually match +// against the error message, Node.js throws an `ERR_AMBIGUOUS_ARGUMENT` error. +assert.throws(throwingSecond, 'Second'); +// TypeError [ERR_AMBIGUOUS_ARGUMENT] + +// The string is only used (as message) in case the function does not throw: +assert.throws(notThrowing, 'Second'); +// AssertionError [ERR_ASSERTION]: Missing expected exception: Second + +// If it was intended to match for the error message do this instead: +// It does not throw because the error messages match. +assert.throws(throwingSecond, /Second$/); + +// If the error message does not match, an AssertionError is thrown. +assert.throws(throwingFirst, /Second$/); +// AssertionError [ERR_ASSERTION] +``` + +```cjs +const assert = require('node:assert/strict'); + +function throwingFirst() { + throw new Error('First'); +} + +function throwingSecond() { + throw new Error('Second'); +} + +function notThrowing() {} + +// The second argument is a string and the input function threw an Error. +// The first case will not throw as it does not match for the error message +// thrown by the input function! +assert.throws(throwingFirst, 'Second'); +// In the next example the message has no benefit over the message from the +// error and since it is not clear if the user intended to actually match +// against the error message, Node.js throws an `ERR_AMBIGUOUS_ARGUMENT` error. +assert.throws(throwingSecond, 'Second'); +// TypeError [ERR_AMBIGUOUS_ARGUMENT] + +// The string is only used (as message) in case the function does not throw: +assert.throws(notThrowing, 'Second'); +// AssertionError [ERR_ASSERTION]: Missing expected exception: Second + +// If it was intended to match for the error message do this instead: +// It does not throw because the error messages match. +assert.throws(throwingSecond, /Second$/); + +// If the error message does not match, an AssertionError is thrown. +assert.throws(throwingFirst, /Second$/); +// AssertionError [ERR_ASSERTION] +``` + +Due to the confusing error-prone notation, avoid a string as the second +argument. + +## `assert.partialDeepStrictEqual(actual, expected[, message])` + + + +> Stability: 1.0 - Early development + +- `actual` {any} +- `expected` {any} +- `message` {string|Error} + +[`assert.partialDeepStrictEqual()`][] Asserts the equivalence between the `actual` and `expected` parameters through a +deep comparison, ensuring that all properties in the `expected` parameter are +present in the `actual` parameter with equivalent values, not allowing type coercion. +The main difference with [`assert.deepStrictEqual()`][] is that [`assert.partialDeepStrictEqual()`][] does not require +all properties in the `actual` parameter to be present in the `expected` parameter. +This method should always pass the same test cases as [`assert.deepStrictEqual()`][], behaving as a super set of it. + +```mjs +import assert from 'node:assert'; + +assert.partialDeepStrictEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); +// OK + +assert.partialDeepStrictEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } }); +// OK + +assert.partialDeepStrictEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 }); +// OK + +assert.partialDeepStrictEqual( + new Set(['value1', 'value2']), + new Set(['value1', 'value2']) +); +// OK + +assert.partialDeepStrictEqual( + new Map([['key1', 'value1']]), + new Map([['key1', 'value1']]) +); +// OK + +assert.partialDeepStrictEqual( + new Uint8Array([1, 2, 3]), + new Uint8Array([1, 2, 3]) +); +// OK + +assert.partialDeepStrictEqual(/abc/, /abc/); +// OK + +assert.partialDeepStrictEqual([{ a: 5 }, { b: 5 }], [{ a: 5 }]); +// OK + +assert.partialDeepStrictEqual( + new Set([{ a: 1 }, { b: 1 }]), + new Set([{ a: 1 }]) +); +// OK + +assert.partialDeepStrictEqual(new Date(0), new Date(0)); +// OK + +assert.partialDeepStrictEqual({ a: 1 }, { a: 1, b: 2 }); +// AssertionError + +assert.partialDeepStrictEqual({ a: 1, b: '2' }, { a: 1, b: 2 }); +// AssertionError + +assert.partialDeepStrictEqual({ a: { b: 2 } }, { a: { b: '2' } }); +// AssertionError +``` + +```cjs +const assert = require('node:assert'); + +assert.partialDeepStrictEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); +// OK + +assert.partialDeepStrictEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } }); +// OK + +assert.partialDeepStrictEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 }); +// OK + +assert.partialDeepStrictEqual([{ a: 5 }, { b: 5 }], [{ a: 5 }]); +// OK + +assert.partialDeepStrictEqual( + new Set([{ a: 1 }, { b: 1 }]), + new Set([{ a: 1 }]) +); +// OK + +assert.partialDeepStrictEqual({ a: 1 }, { a: 1, b: 2 }); +// AssertionError + +assert.partialDeepStrictEqual({ a: 1, b: '2' }, { a: 1, b: 2 }); +// AssertionError + +assert.partialDeepStrictEqual({ a: { b: 2 } }, { a: { b: '2' } }); +// AssertionError +``` + +[Object wrappers]: https://developer.mozilla.org/en-US/docs/Glossary/Primitive#Primitive_wrapper_objects_in_JavaScript +[Object.prototype.toString()]: https://tc39.github.io/ecma262/#sec-object.prototype.tostring +[`!=` operator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Inequality +[`===` operator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality +[`==` operator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Equality +[`AssertionError`]: #class-assertassertionerror +[`CallTracker`]: #class-assertcalltracker +[`Class`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes +[`ERR_INVALID_RETURN_VALUE`]: errors.md#err_invalid_return_value +[`Error.captureStackTrace`]: errors.md#errorcapturestacktracetargetobject-constructoropt +[`Object.is()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is +[`assert.deepEqual()`]: #assertdeepequalactual-expected-message +[`assert.deepStrictEqual()`]: #assertdeepstrictequalactual-expected-message +[`assert.doesNotThrow()`]: #assertdoesnotthrowfn-error-message +[`assert.equal()`]: #assertequalactual-expected-message +[`assert.notDeepEqual()`]: #assertnotdeepequalactual-expected-message +[`assert.notDeepStrictEqual()`]: #assertnotdeepstrictequalactual-expected-message +[`assert.notEqual()`]: #assertnotequalactual-expected-message +[`assert.notStrictEqual()`]: #assertnotstrictequalactual-expected-message +[`assert.ok()`]: #assertokvalue-message +[`assert.partialDeepStrictEqual()`]: #assertpartialdeepstrictequalactual-expected-message +[`assert.strictEqual()`]: #assertstrictequalactual-expected-message +[`assert.throws()`]: #assertthrowsfn-error-message +[`getColorDepth()`]: tty.md#writestreamgetcolordepthenv +[`mock`]: test.md#mocking +[`process.on('exit')`]: process.md#event-exit +[`tracker.calls()`]: #trackercallsfn-exact +[`tracker.verify()`]: #trackerverify +[enumerable "own" properties]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Enumerability_and_ownership_of_properties +[prototype-spec]: https://tc39.github.io/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots diff --git a/src/linter/index.mjs b/src/linter/index.mjs index 616b8b87..e057e659 100644 --- a/src/linter/index.mjs +++ b/src/linter/index.mjs @@ -1,4 +1,3 @@ -// @ts-check 'use strict'; import reporters from './reporters/index.mjs'; diff --git a/src/linter/reporters/console.mjs b/src/linter/reporters/console.mjs index ddad029f..abf26e77 100644 --- a/src/linter/reporters/console.mjs +++ b/src/linter/reporters/console.mjs @@ -1,5 +1,3 @@ -// @ts-check - 'use strict'; import { styleText } from 'node:util'; diff --git a/src/linter/reporters/github.mjs b/src/linter/reporters/github.mjs index 36958de8..1971726a 100644 --- a/src/linter/reporters/github.mjs +++ b/src/linter/reporters/github.mjs @@ -1,22 +1,22 @@ -// @ts-check - 'use strict'; import * as core from '@actions/core'; +const actions = { + warn: core.warning, + error: core.error, + info: core.notice, +}; + /** * GitHub action reporter for * * @type {import('../types.d.ts').Reporter} */ export default issue => { - const actions = { - warn: core.warning, - error: core.error, - info: core.notice, - }; + const logFn = actions[issue.level] || core.notice; - (actions[issue.level] || core.notice)(issue.message, { + logFn(issue.message, { file: issue.location.path, startLine: issue.location.position?.start.line, endLine: issue.location.position?.end.line, From 7dfb6dca6329730d1b546a91c80c4e8fce01d1ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Thu, 20 Feb 2025 12:07:31 -0300 Subject: [PATCH 08/22] fix: missing reporter option --- bin/cli.mjs | 2 +- docs/assert.md | 2686 ------------------- src/constants.mjs | 56 +- src/linter/reporters/index.mjs | 8 +- src/linter/rules/invalid-change-version.mjs | 3 +- src/linter/rules/missing-introduced-in.mjs | 6 +- 6 files changed, 55 insertions(+), 2706 deletions(-) delete mode 100644 docs/assert.md diff --git a/bin/cli.mjs b/bin/cli.mjs index f64d2735..f0c618eb 100755 --- a/bin/cli.mjs +++ b/bin/cli.mjs @@ -54,7 +54,7 @@ program ) .addOption(new Option('--skip-linting', 'Skip linting').default(false)) .addOption( - new Option('--reporter', 'Specify the linter reporter') + new Option('-r, --reporter [reporter]', 'Specify the linter reporter') .choices(Object.keys(reporters)) .default('console') ) diff --git a/docs/assert.md b/docs/assert.md deleted file mode 100644 index 2b61b5b5..00000000 --- a/docs/assert.md +++ /dev/null @@ -1,2686 +0,0 @@ -# Assert - -> Stability: 2 - Stable - - - -The `node:assert` module provides a set of assertion functions for verifying -invariants. - -## Strict assertion mode - - - -In strict assertion mode, non-strict methods behave like their corresponding -strict methods. For example, [`assert.deepEqual()`][] will behave like -[`assert.deepStrictEqual()`][]. - -In strict assertion mode, error messages for objects display a diff. In legacy -assertion mode, error messages for objects display the objects, often truncated. - -To use strict assertion mode: - -```mjs -import { strict as assert } from 'node:assert'; -``` - -```cjs -const assert = require('node:assert').strict; -``` - -```mjs -import assert from 'node:assert/strict'; -``` - -```cjs -const assert = require('node:assert/strict'); -``` - -Example error diff: - -```mjs -import { strict as assert } from 'node:assert'; - -assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]); -// AssertionError: Expected inputs to be strictly deep-equal: -// + actual - expected ... Lines skipped -// -// [ -// [ -// ... -// 2, -// + 3 -// - '3' -// ], -// ... -// 5 -// ] -``` - -```cjs -const assert = require('node:assert/strict'); - -assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]); -// AssertionError: Expected inputs to be strictly deep-equal: -// + actual - expected ... Lines skipped -// -// [ -// [ -// ... -// 2, -// + 3 -// - '3' -// ], -// ... -// 5 -// ] -``` - -To deactivate the colors, use the `NO_COLOR` or `NODE_DISABLE_COLORS` -environment variables. This will also deactivate the colors in the REPL. For -more on color support in terminal environments, read the tty -[`getColorDepth()`][] documentation. - -## Legacy assertion mode - -Legacy assertion mode uses the [`==` operator][] in: - -- [`assert.deepEqual()`][] -- [`assert.equal()`][] -- [`assert.notDeepEqual()`][] -- [`assert.notEqual()`][] - -To use legacy assertion mode: - -```mjs -import assert from 'node:assert'; -``` - -```cjs -const assert = require('node:assert'); -``` - -Legacy assertion mode may have surprising results, especially when using -[`assert.deepEqual()`][]: - -```cjs -// WARNING: This does not throw an AssertionError in legacy assertion mode! -assert.deepEqual(/a/gi, new Date()); -``` - -## Class: assert.AssertionError - -- Extends: {errors.Error} - -Indicates the failure of an assertion. All errors thrown by the `node:assert` -module will be instances of the `AssertionError` class. - -### `new assert.AssertionError(options)` - - - -- `options` {Object} - - `message` {string} If provided, the error message is set to this value. - - `actual` {any} The `actual` property on the error instance. - - `expected` {any} The `expected` property on the error instance. - - `operator` {string} The `operator` property on the error instance. - - `stackStartFn` {Function} If provided, the generated stack trace omits - frames before this function. - -A subclass of {Error} that indicates the failure of an assertion. - -All instances contain the built-in `Error` properties (`message` and `name`) -and: - -- `actual` {any} Set to the `actual` argument for methods such as - [`assert.strictEqual()`][]. -- `expected` {any} Set to the `expected` value for methods such as - [`assert.strictEqual()`][]. -- `generatedMessage` {boolean} Indicates if the message was auto-generated - (`true`) or not. -- `code` {string} Value is always `ERR_ASSERTION` to show that the error is an - assertion error. -- `operator` {string} Set to the passed in operator value. - -```mjs -import assert from 'node:assert'; - -// Generate an AssertionError to compare the error message later: -const { message } = new assert.AssertionError({ - actual: 1, - expected: 2, - operator: 'strictEqual', -}); - -// Verify error output: -try { - assert.strictEqual(1, 2); -} catch (err) { - assert(err instanceof assert.AssertionError); - assert.strictEqual(err.message, message); - assert.strictEqual(err.name, 'AssertionError'); - assert.strictEqual(err.actual, 1); - assert.strictEqual(err.expected, 2); - assert.strictEqual(err.code, 'ERR_ASSERTION'); - assert.strictEqual(err.operator, 'strictEqual'); - assert.strictEqual(err.generatedMessage, true); -} -``` - -```cjs -const assert = require('node:assert'); - -// Generate an AssertionError to compare the error message later: -const { message } = new assert.AssertionError({ - actual: 1, - expected: 2, - operator: 'strictEqual', -}); - -// Verify error output: -try { - assert.strictEqual(1, 2); -} catch (err) { - assert(err instanceof assert.AssertionError); - assert.strictEqual(err.message, message); - assert.strictEqual(err.name, 'AssertionError'); - assert.strictEqual(err.actual, 1); - assert.strictEqual(err.expected, 2); - assert.strictEqual(err.code, 'ERR_ASSERTION'); - assert.strictEqual(err.operator, 'strictEqual'); - assert.strictEqual(err.generatedMessage, true); -} -``` - -## Class: `assert.CallTracker` - - - -> Stability: 0 - Deprecated - -This feature is deprecated and will be removed in a future version. -Please consider using alternatives such as the -[`mock`][] helper function. - -### `new assert.CallTracker()` - - - -Creates a new [`CallTracker`][] object which can be used to track if functions -were called a specific number of times. The `tracker.verify()` must be called -for the verification to take place. The usual pattern would be to call it in a -[`process.on('exit')`][] handler. - -```mjs -import assert from 'node:assert'; -import process from 'node:process'; - -const tracker = new assert.CallTracker(); - -function func() {} - -// callsfunc() must be called exactly 1 time before tracker.verify(). -const callsfunc = tracker.calls(func, 1); - -callsfunc(); - -// Calls tracker.verify() and verifies if all tracker.calls() functions have -// been called exact times. -process.on('exit', () => { - tracker.verify(); -}); -``` - -```cjs -const assert = require('node:assert'); -const process = require('node:process'); - -const tracker = new assert.CallTracker(); - -function func() {} - -// callsfunc() must be called exactly 1 time before tracker.verify(). -const callsfunc = tracker.calls(func, 1); - -callsfunc(); - -// Calls tracker.verify() and verifies if all tracker.calls() functions have -// been called exact times. -process.on('exit', () => { - tracker.verify(); -}); -``` - -### `tracker.calls([fn][, exact])` - - - -- `fn` {Function} **Default:** A no-op function. -- `exact` {number} **Default:** `1`. -- Returns: {Function} A function that wraps `fn`. - -The wrapper function is expected to be called exactly `exact` times. If the -function has not been called exactly `exact` times when -[`tracker.verify()`][] is called, then [`tracker.verify()`][] will throw an -error. - -```mjs -import assert from 'node:assert'; - -// Creates call tracker. -const tracker = new assert.CallTracker(); - -function func() {} - -// Returns a function that wraps func() that must be called exact times -// before tracker.verify(). -const callsfunc = tracker.calls(func); -``` - -```cjs -const assert = require('node:assert'); - -// Creates call tracker. -const tracker = new assert.CallTracker(); - -function func() {} - -// Returns a function that wraps func() that must be called exact times -// before tracker.verify(). -const callsfunc = tracker.calls(func); -``` - -### `tracker.getCalls(fn)` - - - -- `fn` {Function} - -- Returns: {Array} An array with all the calls to a tracked function. - -- Object {Object} - - `thisArg` {Object} - - `arguments` {Array} the arguments passed to the tracked function - -```mjs -import assert from 'node:assert'; - -const tracker = new assert.CallTracker(); - -function func() {} -const callsfunc = tracker.calls(func); -callsfunc(1, 2, 3); - -assert.deepStrictEqual(tracker.getCalls(callsfunc), [ - { thisArg: undefined, arguments: [1, 2, 3] }, -]); -``` - -```cjs -const assert = require('node:assert'); - -// Creates call tracker. -const tracker = new assert.CallTracker(); - -function func() {} -const callsfunc = tracker.calls(func); -callsfunc(1, 2, 3); - -assert.deepStrictEqual(tracker.getCalls(callsfunc), [ - { thisArg: undefined, arguments: [1, 2, 3] }, -]); -``` - -### `tracker.report()` - - - -- Returns: {Array} An array of objects containing information about the wrapper - functions returned by [`tracker.calls()`][]. -- Object {Object} - - `message` {string} - - `actual` {number} The actual number of times the function was called. - - `expected` {number} The number of times the function was expected to be - called. - - `operator` {string} The name of the function that is wrapped. - - `stack` {Object} A stack trace of the function. - -The arrays contains information about the expected and actual number of calls of -the functions that have not been called the expected number of times. - -```mjs -import assert from 'node:assert'; - -// Creates call tracker. -const tracker = new assert.CallTracker(); - -function func() {} - -// Returns a function that wraps func() that must be called exact times -// before tracker.verify(). -const callsfunc = tracker.calls(func, 2); - -// Returns an array containing information on callsfunc() -console.log(tracker.report()); -// [ -// { -// message: 'Expected the func function to be executed 2 time(s) but was -// executed 0 time(s).', -// actual: 0, -// expected: 2, -// operator: 'func', -// stack: stack trace -// } -// ] -``` - -```cjs -const assert = require('node:assert'); - -// Creates call tracker. -const tracker = new assert.CallTracker(); - -function func() {} - -// Returns a function that wraps func() that must be called exact times -// before tracker.verify(). -const callsfunc = tracker.calls(func, 2); - -// Returns an array containing information on callsfunc() -console.log(tracker.report()); -// [ -// { -// message: 'Expected the func function to be executed 2 time(s) but was -// executed 0 time(s).', -// actual: 0, -// expected: 2, -// operator: 'func', -// stack: stack trace -// } -// ] -``` - -### `tracker.reset([fn])` - - - -- `fn` {Function} a tracked function to reset. - -Reset calls of the call tracker. -If a tracked function is passed as an argument, the calls will be reset for it. -If no arguments are passed, all tracked functions will be reset. - -```mjs -import assert from 'node:assert'; - -const tracker = new assert.CallTracker(); - -function func() {} -const callsfunc = tracker.calls(func); - -callsfunc(); -// Tracker was called once -assert.strictEqual(tracker.getCalls(callsfunc).length, 1); - -tracker.reset(callsfunc); -assert.strictEqual(tracker.getCalls(callsfunc).length, 0); -``` - -```cjs -const assert = require('node:assert'); - -const tracker = new assert.CallTracker(); - -function func() {} -const callsfunc = tracker.calls(func); - -callsfunc(); -// Tracker was called once -assert.strictEqual(tracker.getCalls(callsfunc).length, 1); - -tracker.reset(callsfunc); -assert.strictEqual(tracker.getCalls(callsfunc).length, 0); -``` - -### `tracker.verify()` - - - -Iterates through the list of functions passed to -[`tracker.calls()`][] and will throw an error for functions that -have not been called the expected number of times. - -```mjs -import assert from 'node:assert'; - -// Creates call tracker. -const tracker = new assert.CallTracker(); - -function func() {} - -// Returns a function that wraps func() that must be called exact times -// before tracker.verify(). -const callsfunc = tracker.calls(func, 2); - -callsfunc(); - -// Will throw an error since callsfunc() was only called once. -tracker.verify(); -``` - -```cjs -const assert = require('node:assert'); - -// Creates call tracker. -const tracker = new assert.CallTracker(); - -function func() {} - -// Returns a function that wraps func() that must be called exact times -// before tracker.verify(). -const callsfunc = tracker.calls(func, 2); - -callsfunc(); - -// Will throw an error since callsfunc() was only called once. -tracker.verify(); -``` - -## `assert(value[, message])` - - - -- `value` {any} The input that is checked for being truthy. -- `message` {string|Error} - -An alias of [`assert.ok()`][]. - -## `assert.deepEqual(actual, expected[, message])` - - - -- `actual` {any} -- `expected` {any} -- `message` {string|Error} - -**Strict assertion mode** - -An alias of [`assert.deepStrictEqual()`][]. - -**Legacy assertion mode** - -> Stability: 3 - Legacy: Use [`assert.deepStrictEqual()`][] instead. - -Tests for deep equality between the `actual` and `expected` parameters. Consider -using [`assert.deepStrictEqual()`][] instead. [`assert.deepEqual()`][] can have -surprising results. - -_Deep equality_ means that the enumerable "own" properties of child objects -are also recursively evaluated by the following rules. - -### Comparison details - -- Primitive values are compared with the [`==` operator][], - with the exception of {NaN}. It is treated as being identical in case - both sides are {NaN}. -- [Type tags][Object.prototype.toString()] of objects should be the same. -- Only [enumerable "own" properties][] are considered. -- {Error} names, messages, causes, and errors are always compared, - even if these are not enumerable properties. -- [Object wrappers][] are compared both as objects and unwrapped values. -- `Object` properties are compared unordered. -- {Map} keys and {Set} items are compared unordered. -- Recursion stops when both sides differ or both sides encounter a circular - reference. -- Implementation does not test the [`[[Prototype]]`][prototype-spec] of - objects. -- {Symbol} properties are not compared. -- {WeakMap} and {WeakSet} comparison does not rely on their values - but only on their instances. -- {RegExp} lastIndex, flags, and source are always compared, even if these - are not enumerable properties. - -The following example does not throw an [`AssertionError`][] because the -primitives are compared using the [`==` operator][]. - -```mjs -import assert from 'node:assert'; -// WARNING: This does not throw an AssertionError! - -assert.deepEqual('+00000000', false); -``` - -```cjs -const assert = require('node:assert'); -// WARNING: This does not throw an AssertionError! - -assert.deepEqual('+00000000', false); -``` - -"Deep" equality means that the enumerable "own" properties of child objects -are evaluated also: - -```mjs -import assert from 'node:assert'; - -const obj1 = { - a: { - b: 1, - }, -}; -const obj2 = { - a: { - b: 2, - }, -}; -const obj3 = { - a: { - b: 1, - }, -}; -const obj4 = { __proto__: obj1 }; - -assert.deepEqual(obj1, obj1); -// OK - -// Values of b are different: -assert.deepEqual(obj1, obj2); -// AssertionError: { a: { b: 1 } } deepEqual { a: { b: 2 } } - -assert.deepEqual(obj1, obj3); -// OK - -// Prototypes are ignored: -assert.deepEqual(obj1, obj4); -// AssertionError: { a: { b: 1 } } deepEqual {} -``` - -```cjs -const assert = require('node:assert'); - -const obj1 = { - a: { - b: 1, - }, -}; -const obj2 = { - a: { - b: 2, - }, -}; -const obj3 = { - a: { - b: 1, - }, -}; -const obj4 = { __proto__: obj1 }; - -assert.deepEqual(obj1, obj1); -// OK - -// Values of b are different: -assert.deepEqual(obj1, obj2); -// AssertionError: { a: { b: 1 } } deepEqual { a: { b: 2 } } - -assert.deepEqual(obj1, obj3); -// OK - -// Prototypes are ignored: -assert.deepEqual(obj1, obj4); -// AssertionError: { a: { b: 1 } } deepEqual {} -``` - -If the values are not equal, an [`AssertionError`][] is thrown with a `message` -property set equal to the value of the `message` parameter. If the `message` -parameter is undefined, a default error message is assigned. If the `message` -parameter is an instance of {Error} then it will be thrown instead of the -[`AssertionError`][]. - -## `assert.deepStrictEqual(actual, expected[, message])` - - - -- `actual` {any} -- `expected` {any} -- `message` {string|Error} - -Tests for deep equality between the `actual` and `expected` parameters. -"Deep" equality means that the enumerable "own" properties of child objects -are recursively evaluated also by the following rules. - -### Comparison details - -- Primitive values are compared using [`Object.is()`][]. -- [Type tags][Object.prototype.toString()] of objects should be the same. -- [`[[Prototype]]`][prototype-spec] of objects are compared using - the [`===` operator][]. -- Only [enumerable "own" properties][] are considered. -- {Error} names, messages, causes, and errors are always compared, - even if these are not enumerable properties. - `errors` is also compared. -- Enumerable own {Symbol} properties are compared as well. -- [Object wrappers][] are compared both as objects and unwrapped values. -- `Object` properties are compared unordered. -- {Map} keys and {Set} items are compared unordered. -- Recursion stops when both sides differ or both sides encounter a circular - reference. -- {WeakMap} and {WeakSet} instances are **not** compared structurally. - They are only equal if they reference the same object. Any comparison between - different `WeakMap` or `WeakSet` instances will result in inequality, - even if they contain the same entries. -- {RegExp} lastIndex, flags, and source are always compared, even if these - are not enumerable properties. - -```mjs -import assert from 'node:assert/strict'; - -// This fails because 1 !== '1'. -assert.deepStrictEqual({ a: 1 }, { a: '1' }); -// AssertionError: Expected inputs to be strictly deep-equal: -// + actual - expected -// -// { -// + a: 1 -// - a: '1' -// } - -// The following objects don't have own properties -const date = new Date(); -const object = {}; -const fakeDate = {}; -Object.setPrototypeOf(fakeDate, Date.prototype); - -// Different [[Prototype]]: -assert.deepStrictEqual(object, fakeDate); -// AssertionError: Expected inputs to be strictly deep-equal: -// + actual - expected -// -// + {} -// - Date {} - -// Different type tags: -assert.deepStrictEqual(date, fakeDate); -// AssertionError: Expected inputs to be strictly deep-equal: -// + actual - expected -// -// + 2018-04-26T00:49:08.604Z -// - Date {} - -assert.deepStrictEqual(NaN, NaN); -// OK because Object.is(NaN, NaN) is true. - -// Different unwrapped numbers: -assert.deepStrictEqual(new Number(1), new Number(2)); -// AssertionError: Expected inputs to be strictly deep-equal: -// + actual - expected -// -// + [Number: 1] -// - [Number: 2] - -assert.deepStrictEqual(new String('foo'), Object('foo')); -// OK because the object and the string are identical when unwrapped. - -assert.deepStrictEqual(-0, -0); -// OK - -// Different zeros: -assert.deepStrictEqual(0, -0); -// AssertionError: Expected inputs to be strictly deep-equal: -// + actual - expected -// -// + 0 -// - -0 - -const symbol1 = Symbol(); -const symbol2 = Symbol(); -assert.deepStrictEqual({ [symbol1]: 1 }, { [symbol1]: 1 }); -// OK, because it is the same symbol on both objects. - -assert.deepStrictEqual({ [symbol1]: 1 }, { [symbol2]: 1 }); -// AssertionError [ERR_ASSERTION]: Inputs identical but not reference equal: -// -// { -// Symbol(): 1 -// } - -const weakMap1 = new WeakMap(); -const weakMap2 = new WeakMap(); -const obj = {}; - -weakMap1.set(obj, 'value'); -weakMap2.set(obj, 'value'); - -// Comparing different instances fails, even with same contents -assert.deepStrictEqual(weakMap1, weakMap2); -// AssertionError: Values have same structure but are not reference-equal: -// -// WeakMap { -// -// } - -// Comparing the same instance to itself succeeds -assert.deepStrictEqual(weakMap1, weakMap1); -// OK - -const weakSet1 = new WeakSet(); -const weakSet2 = new WeakSet(); -weakSet1.add(obj); -weakSet2.add(obj); - -// Comparing different instances fails, even with same contents -assert.deepStrictEqual(weakSet1, weakSet2); -// AssertionError: Values have same structure but are not reference-equal: -// + actual - expected -// -// WeakSet { -// -// } - -// Comparing the same instance to itself succeeds -assert.deepStrictEqual(weakSet1, weakSet1); -// OK -``` - -```cjs -const assert = require('node:assert/strict'); - -// This fails because 1 !== '1'. -assert.deepStrictEqual({ a: 1 }, { a: '1' }); -// AssertionError: Expected inputs to be strictly deep-equal: -// + actual - expected -// -// { -// + a: 1 -// - a: '1' -// } - -// The following objects don't have own properties -const date = new Date(); -const object = {}; -const fakeDate = {}; -Object.setPrototypeOf(fakeDate, Date.prototype); - -// Different [[Prototype]]: -assert.deepStrictEqual(object, fakeDate); -// AssertionError: Expected inputs to be strictly deep-equal: -// + actual - expected -// -// + {} -// - Date {} - -// Different type tags: -assert.deepStrictEqual(date, fakeDate); -// AssertionError: Expected inputs to be strictly deep-equal: -// + actual - expected -// -// + 2018-04-26T00:49:08.604Z -// - Date {} - -assert.deepStrictEqual(NaN, NaN); -// OK because Object.is(NaN, NaN) is true. - -// Different unwrapped numbers: -assert.deepStrictEqual(new Number(1), new Number(2)); -// AssertionError: Expected inputs to be strictly deep-equal: -// + actual - expected -// -// + [Number: 1] -// - [Number: 2] - -assert.deepStrictEqual(new String('foo'), Object('foo')); -// OK because the object and the string are identical when unwrapped. - -assert.deepStrictEqual(-0, -0); -// OK - -// Different zeros: -assert.deepStrictEqual(0, -0); -// AssertionError: Expected inputs to be strictly deep-equal: -// + actual - expected -// -// + 0 -// - -0 - -const symbol1 = Symbol(); -const symbol2 = Symbol(); -assert.deepStrictEqual({ [symbol1]: 1 }, { [symbol1]: 1 }); -// OK, because it is the same symbol on both objects. - -assert.deepStrictEqual({ [symbol1]: 1 }, { [symbol2]: 1 }); -// AssertionError [ERR_ASSERTION]: Inputs identical but not reference equal: -// -// { -// Symbol(): 1 -// } - -const weakMap1 = new WeakMap(); -const weakMap2 = new WeakMap(); -const obj = {}; - -weakMap1.set(obj, 'value'); -weakMap2.set(obj, 'value'); - -// Comparing different instances fails, even with same contents -assert.deepStrictEqual(weakMap1, weakMap2); -// AssertionError: Values have same structure but are not reference-equal: -// -// WeakMap { -// -// } - -// Comparing the same instance to itself succeeds -assert.deepStrictEqual(weakMap1, weakMap1); -// OK - -const weakSet1 = new WeakSet(); -const weakSet2 = new WeakSet(); -weakSet1.add(obj); -weakSet2.add(obj); - -// Comparing different instances fails, even with same contents -assert.deepStrictEqual(weakSet1, weakSet2); -// AssertionError: Values have same structure but are not reference-equal: -// + actual - expected -// -// WeakSet { -// -// } - -// Comparing the same instance to itself succeeds -assert.deepStrictEqual(weakSet1, weakSet1); -// OK -``` - -If the values are not equal, an [`AssertionError`][] is thrown with a `message` -property set equal to the value of the `message` parameter. If the `message` -parameter is undefined, a default error message is assigned. If the `message` -parameter is an instance of {Error} then it will be thrown instead of the -`AssertionError`. - -## `assert.doesNotMatch(string, regexp[, message])` - - - -- `string` {string} -- `regexp` {RegExp} -- `message` {string|Error} - -Expects the `string` input not to match the regular expression. - -```mjs -import assert from 'node:assert/strict'; - -assert.doesNotMatch('I will fail', /fail/); -// AssertionError [ERR_ASSERTION]: The input was expected to not match the ... - -assert.doesNotMatch(123, /pass/); -// AssertionError [ERR_ASSERTION]: The "string" argument must be of type string. - -assert.doesNotMatch('I will pass', /different/); -// OK -``` - -```cjs -const assert = require('node:assert/strict'); - -assert.doesNotMatch('I will fail', /fail/); -// AssertionError [ERR_ASSERTION]: The input was expected to not match the ... - -assert.doesNotMatch(123, /pass/); -// AssertionError [ERR_ASSERTION]: The "string" argument must be of type string. - -assert.doesNotMatch('I will pass', /different/); -// OK -``` - -If the values do match, or if the `string` argument is of another type than -`string`, an [`AssertionError`][] is thrown with a `message` property set equal -to the value of the `message` parameter. If the `message` parameter is -undefined, a default error message is assigned. If the `message` parameter is an -instance of {Error} then it will be thrown instead of the -[`AssertionError`][]. - -## `assert.doesNotReject(asyncFn[, error][, message])` - - - -- `asyncFn` {Function|Promise} -- `error` {RegExp|Function} -- `message` {string} - -Awaits the `asyncFn` promise or, if `asyncFn` is a function, immediately -calls the function and awaits the returned promise to complete. It will then -check that the promise is not rejected. - -If `asyncFn` is a function and it throws an error synchronously, -`assert.doesNotReject()` will return a rejected `Promise` with that error. If -the function does not return a promise, `assert.doesNotReject()` will return a -rejected `Promise` with an [`ERR_INVALID_RETURN_VALUE`][] error. In both cases -the error handler is skipped. - -Using `assert.doesNotReject()` is actually not useful because there is little -benefit in catching a rejection and then rejecting it again. Instead, consider -adding a comment next to the specific code path that should not reject and keep -error messages as expressive as possible. - -If specified, `error` can be a [`Class`][], {RegExp} or a validation -function. See [`assert.throws()`][] for more details. - -Besides the async nature to await the completion behaves identically to -[`assert.doesNotThrow()`][]. - -```mjs -import assert from 'node:assert/strict'; - -await assert.doesNotReject(async () => { - throw new TypeError('Wrong value'); -}, SyntaxError); -``` - -```cjs -const assert = require('node:assert/strict'); - -(async () => { - await assert.doesNotReject(async () => { - throw new TypeError('Wrong value'); - }, SyntaxError); -})(); -``` - -```mjs -import assert from 'node:assert/strict'; - -assert.doesNotReject(Promise.reject(new TypeError('Wrong value'))).then(() => { - // ... -}); -``` - -```cjs -const assert = require('node:assert/strict'); - -assert.doesNotReject(Promise.reject(new TypeError('Wrong value'))).then(() => { - // ... -}); -``` - -## `assert.doesNotThrow(fn[, error][, message])` - - - -- `fn` {Function} -- `error` {RegExp|Function} -- `message` {string} - -Asserts that the function `fn` does not throw an error. - -Using `assert.doesNotThrow()` is actually not useful because there -is no benefit in catching an error and then rethrowing it. Instead, consider -adding a comment next to the specific code path that should not throw and keep -error messages as expressive as possible. - -When `assert.doesNotThrow()` is called, it will immediately call the `fn` -function. - -If an error is thrown and it is the same type as that specified by the `error` -parameter, then an [`AssertionError`][] is thrown. If the error is of a -different type, or if the `error` parameter is undefined, the error is -propagated back to the caller. - -If specified, `error` can be a [`Class`][], {RegExp}, or a validation -function. See [`assert.throws()`][] for more details. - -The following, for instance, will throw the {TypeError} because there is no -matching error type in the assertion: - -```mjs -import assert from 'node:assert/strict'; - -assert.doesNotThrow(() => { - throw new TypeError('Wrong value'); -}, SyntaxError); -``` - -```cjs -const assert = require('node:assert/strict'); - -assert.doesNotThrow(() => { - throw new TypeError('Wrong value'); -}, SyntaxError); -``` - -However, the following will result in an [`AssertionError`][] with the message -'Got unwanted exception...': - -```mjs -import assert from 'node:assert/strict'; - -assert.doesNotThrow(() => { - throw new TypeError('Wrong value'); -}, TypeError); -``` - -```cjs -const assert = require('node:assert/strict'); - -assert.doesNotThrow(() => { - throw new TypeError('Wrong value'); -}, TypeError); -``` - -If an [`AssertionError`][] is thrown and a value is provided for the `message` -parameter, the value of `message` will be appended to the [`AssertionError`][] -message: - -```mjs -import assert from 'node:assert/strict'; - -assert.doesNotThrow( - () => { - throw new TypeError('Wrong value'); - }, - /Wrong value/, - 'Whoops' -); -// Throws: AssertionError: Got unwanted exception: Whoops -``` - -```cjs -const assert = require('node:assert/strict'); - -assert.doesNotThrow( - () => { - throw new TypeError('Wrong value'); - }, - /Wrong value/, - 'Whoops' -); -// Throws: AssertionError: Got unwanted exception: Whoops -``` - -## `assert.equal(actual, expected[, message])` - - - -- `actual` {any} -- `expected` {any} -- `message` {string|Error} - -**Strict assertion mode** - -An alias of [`assert.strictEqual()`][]. - -**Legacy assertion mode** - -> Stability: 3 - Legacy: Use [`assert.strictEqual()`][] instead. - -Tests shallow, coercive equality between the `actual` and `expected` parameters -using the [`==` operator][]. `NaN` is specially handled -and treated as being identical if both sides are `NaN`. - -```mjs -import assert from 'node:assert'; - -assert.equal(1, 1); -// OK, 1 == 1 -assert.equal(1, '1'); -// OK, 1 == '1' -assert.equal(NaN, NaN); -// OK - -assert.equal(1, 2); -// AssertionError: 1 == 2 -assert.equal({ a: { b: 1 } }, { a: { b: 1 } }); -// AssertionError: { a: { b: 1 } } == { a: { b: 1 } } -``` - -```cjs -const assert = require('node:assert'); - -assert.equal(1, 1); -// OK, 1 == 1 -assert.equal(1, '1'); -// OK, 1 == '1' -assert.equal(NaN, NaN); -// OK - -assert.equal(1, 2); -// AssertionError: 1 == 2 -assert.equal({ a: { b: 1 } }, { a: { b: 1 } }); -// AssertionError: { a: { b: 1 } } == { a: { b: 1 } } -``` - -If the values are not equal, an [`AssertionError`][] is thrown with a `message` -property set equal to the value of the `message` parameter. If the `message` -parameter is undefined, a default error message is assigned. If the `message` -parameter is an instance of {Error} then it will be thrown instead of the -`AssertionError`. - -## `assert.fail([message])` - - - -- `message` {string|Error} **Default:** `'Failed'` - -Throws an [`AssertionError`][] with the provided error message or a default -error message. If the `message` parameter is an instance of {Error} then -it will be thrown instead of the [`AssertionError`][]. - -```mjs -import assert from 'node:assert/strict'; - -assert.fail(); -// AssertionError [ERR_ASSERTION]: Failed - -assert.fail('boom'); -// AssertionError [ERR_ASSERTION]: boom - -assert.fail(new TypeError('need array')); -// TypeError: need array -``` - -```cjs -const assert = require('node:assert/strict'); - -assert.fail(); -// AssertionError [ERR_ASSERTION]: Failed - -assert.fail('boom'); -// AssertionError [ERR_ASSERTION]: boom - -assert.fail(new TypeError('need array')); -// TypeError: need array -``` - -Using `assert.fail()` with more than two arguments is possible but deprecated. -See below for further details. - -## `assert.fail(actual, expected[, message[, operator[, stackStartFn]]])` - - - -> Stability: 0 - Deprecated: Use `assert.fail([message])` or other assert -> functions instead. - -- `actual` {any} -- `expected` {any} -- `message` {string|Error} -- `operator` {string} **Default:** `'!='` -- `stackStartFn` {Function} **Default:** `assert.fail` - -If `message` is falsy, the error message is set as the values of `actual` and -`expected` separated by the provided `operator`. If just the two `actual` and -`expected` arguments are provided, `operator` will default to `'!='`. If -`message` is provided as third argument it will be used as the error message and -the other arguments will be stored as properties on the thrown object. If -`stackStartFn` is provided, all stack frames above that function will be -removed from stacktrace (see [`Error.captureStackTrace`][]). If no arguments are -given, the default message `Failed` will be used. - -```mjs -import assert from 'node:assert/strict'; - -assert.fail('a', 'b'); -// AssertionError [ERR_ASSERTION]: 'a' != 'b' - -assert.fail(1, 2, undefined, '>'); -// AssertionError [ERR_ASSERTION]: 1 > 2 - -assert.fail(1, 2, 'fail'); -// AssertionError [ERR_ASSERTION]: fail - -assert.fail(1, 2, 'whoops', '>'); -// AssertionError [ERR_ASSERTION]: whoops - -assert.fail(1, 2, new TypeError('need array')); -// TypeError: need array -``` - -```cjs -const assert = require('node:assert/strict'); - -assert.fail('a', 'b'); -// AssertionError [ERR_ASSERTION]: 'a' != 'b' - -assert.fail(1, 2, undefined, '>'); -// AssertionError [ERR_ASSERTION]: 1 > 2 - -assert.fail(1, 2, 'fail'); -// AssertionError [ERR_ASSERTION]: fail - -assert.fail(1, 2, 'whoops', '>'); -// AssertionError [ERR_ASSERTION]: whoops - -assert.fail(1, 2, new TypeError('need array')); -// TypeError: need array -``` - -In the last three cases `actual`, `expected`, and `operator` have no -influence on the error message. - -Example use of `stackStartFn` for truncating the exception's stacktrace: - -```mjs -import assert from 'node:assert/strict'; - -function suppressFrame() { - assert.fail('a', 'b', undefined, '!==', suppressFrame); -} -suppressFrame(); -// AssertionError [ERR_ASSERTION]: 'a' !== 'b' -// at repl:1:1 -// at ContextifyScript.Script.runInThisContext (vm.js:44:33) -// ... -``` - -```cjs -const assert = require('node:assert/strict'); - -function suppressFrame() { - assert.fail('a', 'b', undefined, '!==', suppressFrame); -} -suppressFrame(); -// AssertionError [ERR_ASSERTION]: 'a' !== 'b' -// at repl:1:1 -// at ContextifyScript.Script.runInThisContext (vm.js:44:33) -// ... -``` - -## `assert.ifError(value)` - - - -- `value` {any} - -Throws `value` if `value` is not `undefined` or `null`. This is useful when -testing the `error` argument in callbacks. The stack trace contains all frames -from the error passed to `ifError()` including the potential new frames for -`ifError()` itself. - -```mjs -import assert from 'node:assert/strict'; - -assert.ifError(null); -// OK -assert.ifError(0); -// AssertionError [ERR_ASSERTION]: ifError got unwanted exception: 0 -assert.ifError('error'); -// AssertionError [ERR_ASSERTION]: ifError got unwanted exception: 'error' -assert.ifError(new Error()); -// AssertionError [ERR_ASSERTION]: ifError got unwanted exception: Error - -// Create some random error frames. -let err; -(function errorFrame() { - err = new Error('test error'); -})(); - -(function ifErrorFrame() { - assert.ifError(err); -})(); -// AssertionError [ERR_ASSERTION]: ifError got unwanted exception: test error -// at ifErrorFrame -// at errorFrame -``` - -```cjs -const assert = require('node:assert/strict'); - -assert.ifError(null); -// OK -assert.ifError(0); -// AssertionError [ERR_ASSERTION]: ifError got unwanted exception: 0 -assert.ifError('error'); -// AssertionError [ERR_ASSERTION]: ifError got unwanted exception: 'error' -assert.ifError(new Error()); -// AssertionError [ERR_ASSERTION]: ifError got unwanted exception: Error - -// Create some random error frames. -let err; -(function errorFrame() { - err = new Error('test error'); -})(); - -(function ifErrorFrame() { - assert.ifError(err); -})(); -// AssertionError [ERR_ASSERTION]: ifError got unwanted exception: test error -// at ifErrorFrame -// at errorFrame -``` - -## `assert.match(string, regexp[, message])` - - - -- `string` {string} -- `regexp` {RegExp} -- `message` {string|Error} - -Expects the `string` input to match the regular expression. - -```mjs -import assert from 'node:assert/strict'; - -assert.match('I will fail', /pass/); -// AssertionError [ERR_ASSERTION]: The input did not match the regular ... - -assert.match(123, /pass/); -// AssertionError [ERR_ASSERTION]: The "string" argument must be of type string. - -assert.match('I will pass', /pass/); -// OK -``` - -```cjs -const assert = require('node:assert/strict'); - -assert.match('I will fail', /pass/); -// AssertionError [ERR_ASSERTION]: The input did not match the regular ... - -assert.match(123, /pass/); -// AssertionError [ERR_ASSERTION]: The "string" argument must be of type string. - -assert.match('I will pass', /pass/); -// OK -``` - -If the values do not match, or if the `string` argument is of another type than -`string`, an [`AssertionError`][] is thrown with a `message` property set equal -to the value of the `message` parameter. If the `message` parameter is -undefined, a default error message is assigned. If the `message` parameter is an -instance of {Error} then it will be thrown instead of the -[`AssertionError`][]. - -## `assert.notDeepEqual(actual, expected[, message])` - - - -- `actual` {any} -- `expected` {any} -- `message` {string|Error} - -**Strict assertion mode** - -An alias of [`assert.notDeepStrictEqual()`][]. - -**Legacy assertion mode** - -> Stability: 3 - Legacy: Use [`assert.notDeepStrictEqual()`][] instead. - -Tests for any deep inequality. Opposite of [`assert.deepEqual()`][]. - -```mjs -import assert from 'node:assert'; - -const obj1 = { - a: { - b: 1, - }, -}; -const obj2 = { - a: { - b: 2, - }, -}; -const obj3 = { - a: { - b: 1, - }, -}; -const obj4 = { __proto__: obj1 }; - -assert.notDeepEqual(obj1, obj1); -// AssertionError: { a: { b: 1 } } notDeepEqual { a: { b: 1 } } - -assert.notDeepEqual(obj1, obj2); -// OK - -assert.notDeepEqual(obj1, obj3); -// AssertionError: { a: { b: 1 } } notDeepEqual { a: { b: 1 } } - -assert.notDeepEqual(obj1, obj4); -// OK -``` - -```cjs -const assert = require('node:assert'); - -const obj1 = { - a: { - b: 1, - }, -}; -const obj2 = { - a: { - b: 2, - }, -}; -const obj3 = { - a: { - b: 1, - }, -}; -const obj4 = { __proto__: obj1 }; - -assert.notDeepEqual(obj1, obj1); -// AssertionError: { a: { b: 1 } } notDeepEqual { a: { b: 1 } } - -assert.notDeepEqual(obj1, obj2); -// OK - -assert.notDeepEqual(obj1, obj3); -// AssertionError: { a: { b: 1 } } notDeepEqual { a: { b: 1 } } - -assert.notDeepEqual(obj1, obj4); -// OK -``` - -If the values are deeply equal, an [`AssertionError`][] is thrown with a -`message` property set equal to the value of the `message` parameter. If the -`message` parameter is undefined, a default error message is assigned. If the -`message` parameter is an instance of {Error} then it will be thrown -instead of the `AssertionError`. - -## `assert.notDeepStrictEqual(actual, expected[, message])` - - - -- `actual` {any} -- `expected` {any} -- `message` {string|Error} - -Tests for deep strict inequality. Opposite of [`assert.deepStrictEqual()`][]. - -```mjs -import assert from 'node:assert/strict'; - -assert.notDeepStrictEqual({ a: 1 }, { a: '1' }); -// OK -``` - -```cjs -const assert = require('node:assert/strict'); - -assert.notDeepStrictEqual({ a: 1 }, { a: '1' }); -// OK -``` - -If the values are deeply and strictly equal, an [`AssertionError`][] is thrown -with a `message` property set equal to the value of the `message` parameter. If -the `message` parameter is undefined, a default error message is assigned. If -the `message` parameter is an instance of {Error} then it will be thrown -instead of the [`AssertionError`][]. - -## `assert.notEqual(actual, expected[, message])` - - - -- `actual` {any} -- `expected` {any} -- `message` {string|Error} - -**Strict assertion mode** - -An alias of [`assert.notStrictEqual()`][]. - -**Legacy assertion mode** - -> Stability: 3 - Legacy: Use [`assert.notStrictEqual()`][] instead. - -Tests shallow, coercive inequality with the [`!=` operator][]. `NaN` is -specially handled and treated as being identical if both sides are `NaN`. - -```mjs -import assert from 'node:assert'; - -assert.notEqual(1, 2); -// OK - -assert.notEqual(1, 1); -// AssertionError: 1 != 1 - -assert.notEqual(1, '1'); -// AssertionError: 1 != '1' -``` - -```cjs -const assert = require('node:assert'); - -assert.notEqual(1, 2); -// OK - -assert.notEqual(1, 1); -// AssertionError: 1 != 1 - -assert.notEqual(1, '1'); -// AssertionError: 1 != '1' -``` - -If the values are equal, an [`AssertionError`][] is thrown with a `message` -property set equal to the value of the `message` parameter. If the `message` -parameter is undefined, a default error message is assigned. If the `message` -parameter is an instance of {Error} then it will be thrown instead of the -`AssertionError`. - -## `assert.notStrictEqual(actual, expected[, message])` - - - -- `actual` {any} -- `expected` {any} -- `message` {string|Error} - -Tests strict inequality between the `actual` and `expected` parameters as -determined by [`Object.is()`][]. - -```mjs -import assert from 'node:assert/strict'; - -assert.notStrictEqual(1, 2); -// OK - -assert.notStrictEqual(1, 1); -// AssertionError [ERR_ASSERTION]: Expected "actual" to be strictly unequal to: -// -// 1 - -assert.notStrictEqual(1, '1'); -// OK -``` - -```cjs -const assert = require('node:assert/strict'); - -assert.notStrictEqual(1, 2); -// OK - -assert.notStrictEqual(1, 1); -// AssertionError [ERR_ASSERTION]: Expected "actual" to be strictly unequal to: -// -// 1 - -assert.notStrictEqual(1, '1'); -// OK -``` - -If the values are strictly equal, an [`AssertionError`][] is thrown with a -`message` property set equal to the value of the `message` parameter. If the -`message` parameter is undefined, a default error message is assigned. If the -`message` parameter is an instance of {Error} then it will be thrown -instead of the `AssertionError`. - -## `assert.ok(value[, message])` - - - -- `value` {any} -- `message` {string|Error} - -Tests if `value` is truthy. It is equivalent to -`assert.equal(!!value, true, message)`. - -If `value` is not truthy, an [`AssertionError`][] is thrown with a `message` -property set equal to the value of the `message` parameter. If the `message` -parameter is `undefined`, a default error message is assigned. If the `message` -parameter is an instance of {Error} then it will be thrown instead of the -`AssertionError`. -If no arguments are passed in at all `message` will be set to the string: -``'No value argument passed to `assert.ok()`'``. - -Be aware that in the `repl` the error message will be different to the one -thrown in a file! See below for further details. - -```mjs -import assert from 'node:assert/strict'; - -assert.ok(true); -// OK -assert.ok(1); -// OK - -assert.ok(); -// AssertionError: No value argument passed to `assert.ok()` - -assert.ok(false, "it's false"); -// AssertionError: it's false - -// In the repl: -assert.ok(typeof 123 === 'string'); -// AssertionError: false == true - -// In a file (e.g. test.js): -assert.ok(typeof 123 === 'string'); -// AssertionError: The expression evaluated to a falsy value: -// -// assert.ok(typeof 123 === 'string') - -assert.ok(false); -// AssertionError: The expression evaluated to a falsy value: -// -// assert.ok(false) - -assert.ok(0); -// AssertionError: The expression evaluated to a falsy value: -// -// assert.ok(0) -``` - -```cjs -const assert = require('node:assert/strict'); - -assert.ok(true); -// OK -assert.ok(1); -// OK - -assert.ok(); -// AssertionError: No value argument passed to `assert.ok()` - -assert.ok(false, "it's false"); -// AssertionError: it's false - -// In the repl: -assert.ok(typeof 123 === 'string'); -// AssertionError: false == true - -// In a file (e.g. test.js): -assert.ok(typeof 123 === 'string'); -// AssertionError: The expression evaluated to a falsy value: -// -// assert.ok(typeof 123 === 'string') - -assert.ok(false); -// AssertionError: The expression evaluated to a falsy value: -// -// assert.ok(false) - -assert.ok(0); -// AssertionError: The expression evaluated to a falsy value: -// -// assert.ok(0) -``` - -```mjs -import assert from 'node:assert/strict'; - -// Using `assert()` works the same: -assert(0); -// AssertionError: The expression evaluated to a falsy value: -// -// assert(0) -``` - -```cjs -const assert = require('node:assert'); - -// Using `assert()` works the same: -assert(0); -// AssertionError: The expression evaluated to a falsy value: -// -// assert(0) -``` - -## `assert.rejects(asyncFn[, error][, message])` - - - -- `asyncFn` {Function|Promise} -- `error` {RegExp|Function|Object|Error} -- `message` {string} - -Awaits the `asyncFn` promise or, if `asyncFn` is a function, immediately -calls the function and awaits the returned promise to complete. It will then -check that the promise is rejected. - -If `asyncFn` is a function and it throws an error synchronously, -`assert.rejects()` will return a rejected `Promise` with that error. If the -function does not return a promise, `assert.rejects()` will return a rejected -`Promise` with an [`ERR_INVALID_RETURN_VALUE`][] error. In both cases the error -handler is skipped. - -Besides the async nature to await the completion behaves identically to -[`assert.throws()`][]. - -If specified, `error` can be a [`Class`][], {RegExp}, a validation function, -an object where each property will be tested for, or an instance of error where -each property will be tested for including the non-enumerable `message` and -`name` properties. - -If specified, `message` will be the message provided by the [`AssertionError`][] -if the `asyncFn` fails to reject. - -```mjs -import assert from 'node:assert/strict'; - -await assert.rejects( - async () => { - throw new TypeError('Wrong value'); - }, - { - name: 'TypeError', - message: 'Wrong value', - } -); -``` - -```cjs -const assert = require('node:assert/strict'); - -(async () => { - await assert.rejects( - async () => { - throw new TypeError('Wrong value'); - }, - { - name: 'TypeError', - message: 'Wrong value', - } - ); -})(); -``` - -```mjs -import assert from 'node:assert/strict'; - -await assert.rejects( - async () => { - throw new TypeError('Wrong value'); - }, - err => { - assert.strictEqual(err.name, 'TypeError'); - assert.strictEqual(err.message, 'Wrong value'); - return true; - } -); -``` - -```cjs -const assert = require('node:assert/strict'); - -(async () => { - await assert.rejects( - async () => { - throw new TypeError('Wrong value'); - }, - err => { - assert.strictEqual(err.name, 'TypeError'); - assert.strictEqual(err.message, 'Wrong value'); - return true; - } - ); -})(); -``` - -```mjs -import assert from 'node:assert/strict'; - -assert.rejects(Promise.reject(new Error('Wrong value')), Error).then(() => { - // ... -}); -``` - -```cjs -const assert = require('node:assert/strict'); - -assert.rejects(Promise.reject(new Error('Wrong value')), Error).then(() => { - // ... -}); -``` - -`error` cannot be a string. If a string is provided as the second -argument, then `error` is assumed to be omitted and the string will be used for -`message` instead. This can lead to easy-to-miss mistakes. Please read the -example in [`assert.throws()`][] carefully if using a string as the second -argument gets considered. - -## `assert.strictEqual(actual, expected[, message])` - - - -- `actual` {any} -- `expected` {any} -- `message` {string|Error} - -Tests strict equality between the `actual` and `expected` parameters as -determined by [`Object.is()`][]. - -```mjs -import assert from 'node:assert/strict'; - -assert.strictEqual(1, 2); -// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal: -// -// 1 !== 2 - -assert.strictEqual(1, 1); -// OK - -assert.strictEqual('Hello foobar', 'Hello World!'); -// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal: -// + actual - expected -// -// + 'Hello foobar' -// - 'Hello World!' -// ^ - -const apples = 1; -const oranges = 2; -assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`); -// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2 - -assert.strictEqual(1, '1', new TypeError('Inputs are not identical')); -// TypeError: Inputs are not identical -``` - -```cjs -const assert = require('node:assert/strict'); - -assert.strictEqual(1, 2); -// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal: -// -// 1 !== 2 - -assert.strictEqual(1, 1); -// OK - -assert.strictEqual('Hello foobar', 'Hello World!'); -// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal: -// + actual - expected -// -// + 'Hello foobar' -// - 'Hello World!' -// ^ - -const apples = 1; -const oranges = 2; -assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`); -// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2 - -assert.strictEqual(1, '1', new TypeError('Inputs are not identical')); -// TypeError: Inputs are not identical -``` - -If the values are not strictly equal, an [`AssertionError`][] is thrown with a -`message` property set equal to the value of the `message` parameter. If the -`message` parameter is undefined, a default error message is assigned. If the -`message` parameter is an instance of {Error} then it will be thrown -instead of the [`AssertionError`][]. - -## `assert.throws(fn[, error][, message])` - - - -- `fn` {Function} -- `error` {RegExp|Function|Object|Error} -- `message` {string} - -Expects the function `fn` to throw an error. - -If specified, `error` can be a [`Class`][], {RegExp}, a validation function, -a validation object where each property will be tested for strict deep equality, -or an instance of error where each property will be tested for strict deep -equality including the non-enumerable `message` and `name` properties. When -using an object, it is also possible to use a regular expression, when -validating against a string property. See below for examples. - -If specified, `message` will be appended to the message provided by the -`AssertionError` if the `fn` call fails to throw or in case the error validation -fails. - -Custom validation object/error instance: - -```mjs -import assert from 'node:assert/strict'; - -const err = new TypeError('Wrong value'); -err.code = 404; -err.foo = 'bar'; -err.info = { - nested: true, - baz: 'text', -}; -err.reg = /abc/i; - -assert.throws( - () => { - throw err; - }, - { - name: 'TypeError', - message: 'Wrong value', - info: { - nested: true, - baz: 'text', - }, - // Only properties on the validation object will be tested for. - // Using nested objects requires all properties to be present. Otherwise - // the validation is going to fail. - } -); - -// Using regular expressions to validate error properties: -assert.throws( - () => { - throw err; - }, - { - // The `name` and `message` properties are strings and using regular - // expressions on those will match against the string. If they fail, an - // error is thrown. - name: /^TypeError$/, - message: /Wrong/, - foo: 'bar', - info: { - nested: true, - // It is not possible to use regular expressions for nested properties! - baz: 'text', - }, - // The `reg` property contains a regular expression and only if the - // validation object contains an identical regular expression, it is going - // to pass. - reg: /abc/i, - } -); - -// Fails due to the different `message` and `name` properties: -assert.throws( - () => { - const otherErr = new Error('Not found'); - // Copy all enumerable properties from `err` to `otherErr`. - for (const [key, value] of Object.entries(err)) { - otherErr[key] = value; - } - throw otherErr; - }, - // The error's `message` and `name` properties will also be checked when using - // an error as validation object. - err -); -``` - -```cjs -const assert = require('node:assert/strict'); - -const err = new TypeError('Wrong value'); -err.code = 404; -err.foo = 'bar'; -err.info = { - nested: true, - baz: 'text', -}; -err.reg = /abc/i; - -assert.throws( - () => { - throw err; - }, - { - name: 'TypeError', - message: 'Wrong value', - info: { - nested: true, - baz: 'text', - }, - // Only properties on the validation object will be tested for. - // Using nested objects requires all properties to be present. Otherwise - // the validation is going to fail. - } -); - -// Using regular expressions to validate error properties: -assert.throws( - () => { - throw err; - }, - { - // The `name` and `message` properties are strings and using regular - // expressions on those will match against the string. If they fail, an - // error is thrown. - name: /^TypeError$/, - message: /Wrong/, - foo: 'bar', - info: { - nested: true, - // It is not possible to use regular expressions for nested properties! - baz: 'text', - }, - // The `reg` property contains a regular expression and only if the - // validation object contains an identical regular expression, it is going - // to pass. - reg: /abc/i, - } -); - -// Fails due to the different `message` and `name` properties: -assert.throws( - () => { - const otherErr = new Error('Not found'); - // Copy all enumerable properties from `err` to `otherErr`. - for (const [key, value] of Object.entries(err)) { - otherErr[key] = value; - } - throw otherErr; - }, - // The error's `message` and `name` properties will also be checked when using - // an error as validation object. - err -); -``` - -Validate instanceof using constructor: - -```mjs -import assert from 'node:assert/strict'; - -assert.throws(() => { - throw new Error('Wrong value'); -}, Error); -``` - -```cjs -const assert = require('node:assert/strict'); - -assert.throws(() => { - throw new Error('Wrong value'); -}, Error); -``` - -Validate error message using {RegExp}: - -Using a regular expression runs `.toString` on the error object, and will -therefore also include the error name. - -```mjs -import assert from 'node:assert/strict'; - -assert.throws(() => { - throw new Error('Wrong value'); -}, /^Error: Wrong value$/); -``` - -```cjs -const assert = require('node:assert/strict'); - -assert.throws(() => { - throw new Error('Wrong value'); -}, /^Error: Wrong value$/); -``` - -Custom error validation: - -The function must return `true` to indicate all internal validations passed. -It will otherwise fail with an [`AssertionError`][]. - -```mjs -import assert from 'node:assert/strict'; - -assert.throws( - () => { - throw new Error('Wrong value'); - }, - err => { - assert(err instanceof Error); - assert(/value/.test(err)); - // Avoid returning anything from validation functions besides `true`. - // Otherwise, it's not clear what part of the validation failed. Instead, - // throw an error about the specific validation that failed (as done in this - // example) and add as much helpful debugging information to that error as - // possible. - return true; - }, - 'unexpected error' -); -``` - -```cjs -const assert = require('node:assert/strict'); - -assert.throws( - () => { - throw new Error('Wrong value'); - }, - err => { - assert(err instanceof Error); - assert(/value/.test(err)); - // Avoid returning anything from validation functions besides `true`. - // Otherwise, it's not clear what part of the validation failed. Instead, - // throw an error about the specific validation that failed (as done in this - // example) and add as much helpful debugging information to that error as - // possible. - return true; - }, - 'unexpected error' -); -``` - -`error` cannot be a string. If a string is provided as the second -argument, then `error` is assumed to be omitted and the string will be used for -`message` instead. This can lead to easy-to-miss mistakes. Using the same -message as the thrown error message is going to result in an -`ERR_AMBIGUOUS_ARGUMENT` error. Please read the example below carefully if using -a string as the second argument gets considered: - -```mjs -import assert from 'node:assert/strict'; - -function throwingFirst() { - throw new Error('First'); -} - -function throwingSecond() { - throw new Error('Second'); -} - -function notThrowing() {} - -// The second argument is a string and the input function threw an Error. -// The first case will not throw as it does not match for the error message -// thrown by the input function! -assert.throws(throwingFirst, 'Second'); -// In the next example the message has no benefit over the message from the -// error and since it is not clear if the user intended to actually match -// against the error message, Node.js throws an `ERR_AMBIGUOUS_ARGUMENT` error. -assert.throws(throwingSecond, 'Second'); -// TypeError [ERR_AMBIGUOUS_ARGUMENT] - -// The string is only used (as message) in case the function does not throw: -assert.throws(notThrowing, 'Second'); -// AssertionError [ERR_ASSERTION]: Missing expected exception: Second - -// If it was intended to match for the error message do this instead: -// It does not throw because the error messages match. -assert.throws(throwingSecond, /Second$/); - -// If the error message does not match, an AssertionError is thrown. -assert.throws(throwingFirst, /Second$/); -// AssertionError [ERR_ASSERTION] -``` - -```cjs -const assert = require('node:assert/strict'); - -function throwingFirst() { - throw new Error('First'); -} - -function throwingSecond() { - throw new Error('Second'); -} - -function notThrowing() {} - -// The second argument is a string and the input function threw an Error. -// The first case will not throw as it does not match for the error message -// thrown by the input function! -assert.throws(throwingFirst, 'Second'); -// In the next example the message has no benefit over the message from the -// error and since it is not clear if the user intended to actually match -// against the error message, Node.js throws an `ERR_AMBIGUOUS_ARGUMENT` error. -assert.throws(throwingSecond, 'Second'); -// TypeError [ERR_AMBIGUOUS_ARGUMENT] - -// The string is only used (as message) in case the function does not throw: -assert.throws(notThrowing, 'Second'); -// AssertionError [ERR_ASSERTION]: Missing expected exception: Second - -// If it was intended to match for the error message do this instead: -// It does not throw because the error messages match. -assert.throws(throwingSecond, /Second$/); - -// If the error message does not match, an AssertionError is thrown. -assert.throws(throwingFirst, /Second$/); -// AssertionError [ERR_ASSERTION] -``` - -Due to the confusing error-prone notation, avoid a string as the second -argument. - -## `assert.partialDeepStrictEqual(actual, expected[, message])` - - - -> Stability: 1.0 - Early development - -- `actual` {any} -- `expected` {any} -- `message` {string|Error} - -[`assert.partialDeepStrictEqual()`][] Asserts the equivalence between the `actual` and `expected` parameters through a -deep comparison, ensuring that all properties in the `expected` parameter are -present in the `actual` parameter with equivalent values, not allowing type coercion. -The main difference with [`assert.deepStrictEqual()`][] is that [`assert.partialDeepStrictEqual()`][] does not require -all properties in the `actual` parameter to be present in the `expected` parameter. -This method should always pass the same test cases as [`assert.deepStrictEqual()`][], behaving as a super set of it. - -```mjs -import assert from 'node:assert'; - -assert.partialDeepStrictEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); -// OK - -assert.partialDeepStrictEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } }); -// OK - -assert.partialDeepStrictEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 }); -// OK - -assert.partialDeepStrictEqual( - new Set(['value1', 'value2']), - new Set(['value1', 'value2']) -); -// OK - -assert.partialDeepStrictEqual( - new Map([['key1', 'value1']]), - new Map([['key1', 'value1']]) -); -// OK - -assert.partialDeepStrictEqual( - new Uint8Array([1, 2, 3]), - new Uint8Array([1, 2, 3]) -); -// OK - -assert.partialDeepStrictEqual(/abc/, /abc/); -// OK - -assert.partialDeepStrictEqual([{ a: 5 }, { b: 5 }], [{ a: 5 }]); -// OK - -assert.partialDeepStrictEqual( - new Set([{ a: 1 }, { b: 1 }]), - new Set([{ a: 1 }]) -); -// OK - -assert.partialDeepStrictEqual(new Date(0), new Date(0)); -// OK - -assert.partialDeepStrictEqual({ a: 1 }, { a: 1, b: 2 }); -// AssertionError - -assert.partialDeepStrictEqual({ a: 1, b: '2' }, { a: 1, b: 2 }); -// AssertionError - -assert.partialDeepStrictEqual({ a: { b: 2 } }, { a: { b: '2' } }); -// AssertionError -``` - -```cjs -const assert = require('node:assert'); - -assert.partialDeepStrictEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); -// OK - -assert.partialDeepStrictEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } }); -// OK - -assert.partialDeepStrictEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 }); -// OK - -assert.partialDeepStrictEqual([{ a: 5 }, { b: 5 }], [{ a: 5 }]); -// OK - -assert.partialDeepStrictEqual( - new Set([{ a: 1 }, { b: 1 }]), - new Set([{ a: 1 }]) -); -// OK - -assert.partialDeepStrictEqual({ a: 1 }, { a: 1, b: 2 }); -// AssertionError - -assert.partialDeepStrictEqual({ a: 1, b: '2' }, { a: 1, b: 2 }); -// AssertionError - -assert.partialDeepStrictEqual({ a: { b: 2 } }, { a: { b: '2' } }); -// AssertionError -``` - -[Object wrappers]: https://developer.mozilla.org/en-US/docs/Glossary/Primitive#Primitive_wrapper_objects_in_JavaScript -[Object.prototype.toString()]: https://tc39.github.io/ecma262/#sec-object.prototype.tostring -[`!=` operator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Inequality -[`===` operator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality -[`==` operator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Equality -[`AssertionError`]: #class-assertassertionerror -[`CallTracker`]: #class-assertcalltracker -[`Class`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes -[`ERR_INVALID_RETURN_VALUE`]: errors.md#err_invalid_return_value -[`Error.captureStackTrace`]: errors.md#errorcapturestacktracetargetobject-constructoropt -[`Object.is()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is -[`assert.deepEqual()`]: #assertdeepequalactual-expected-message -[`assert.deepStrictEqual()`]: #assertdeepstrictequalactual-expected-message -[`assert.doesNotThrow()`]: #assertdoesnotthrowfn-error-message -[`assert.equal()`]: #assertequalactual-expected-message -[`assert.notDeepEqual()`]: #assertnotdeepequalactual-expected-message -[`assert.notDeepStrictEqual()`]: #assertnotdeepstrictequalactual-expected-message -[`assert.notEqual()`]: #assertnotequalactual-expected-message -[`assert.notStrictEqual()`]: #assertnotstrictequalactual-expected-message -[`assert.ok()`]: #assertokvalue-message -[`assert.partialDeepStrictEqual()`]: #assertpartialdeepstrictequalactual-expected-message -[`assert.strictEqual()`]: #assertstrictequalactual-expected-message -[`assert.throws()`]: #assertthrowsfn-error-message -[`getColorDepth()`]: tty.md#writestreamgetcolordepthenv -[`mock`]: test.md#mocking -[`process.on('exit')`]: process.md#event-exit -[`tracker.calls()`]: #trackercallsfn-exact -[`tracker.verify()`]: #trackerverify -[enumerable "own" properties]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Enumerability_and_ownership_of_properties -[prototype-spec]: https://tc39.github.io/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots diff --git a/src/constants.mjs b/src/constants.mjs index 803bd2db..473cf35b 100644 --- a/src/constants.mjs +++ b/src/constants.mjs @@ -125,17 +125,45 @@ export const DOC_SLUG_ENVIRONMENT = 'environment-variables-1'; // JavaScript globals types within the MDN JavaScript docs // @see DOC_MDN_BASE_URL_JS_GLOBALS export const DOC_TYPES_MAPPING_GLOBALS = { - ...Object.fromEntries([ - 'AggregateError', 'Array', 'ArrayBuffer', 'DataView', 'Date', 'Error', - 'EvalError', 'Function', 'Map', 'NaN', 'Object', 'Promise', 'Proxy', 'RangeError', - 'ReferenceError', 'RegExp', 'Set', 'SharedArrayBuffer', 'SyntaxError', 'Symbol', - 'TypeError', 'URIError', 'WeakMap', 'WeakSet', - - 'TypedArray', - 'Float32Array', 'Float64Array', - 'Int8Array', 'Int16Array', 'Int32Array', - 'Uint8Array', 'Uint8ClampedArray', 'Uint16Array', 'Uint32Array', - ].map(e => [e, e])), + ...Object.fromEntries( + [ + 'AggregateError', + 'Array', + 'ArrayBuffer', + 'DataView', + 'Date', + 'Error', + 'EvalError', + 'Function', + 'Map', + 'NaN', + 'Object', + 'Promise', + 'Proxy', + 'RangeError', + 'ReferenceError', + 'RegExp', + 'Set', + 'SharedArrayBuffer', + 'SyntaxError', + 'Symbol', + 'TypeError', + 'URIError', + 'WeakMap', + 'WeakSet', + + 'TypedArray', + 'Float32Array', + 'Float64Array', + 'Int8Array', + 'Int16Array', + 'Int32Array', + 'Uint8Array', + 'Uint8ClampedArray', + 'Uint16Array', + 'Uint32Array', + ].map(e => [e, e]) + ), bigint: 'BigInt', 'WebAssembly.Instance': 'WebAssembly/Instance', }; @@ -392,3 +420,9 @@ export const DOC_TYPES_MAPPING_OTHER = { Response: `${DOC_MDN_BASE_URL}/API/Response`, Request: `${DOC_MDN_BASE_URL}/API/Request`, }; + +export const LINT_MESSAGES = { + missingIntroducedIn: "Missing 'introduced_in' field in the API doc entry", + missingChangeVersion: 'Missing version field in the API doc entry', + invalidChangeVersion: 'Invalid version number: {{version}}', +}; diff --git a/src/linter/reporters/index.mjs b/src/linter/reporters/index.mjs index efaa58f4..a49d9b6b 100644 --- a/src/linter/reporters/index.mjs +++ b/src/linter/reporters/index.mjs @@ -3,7 +3,7 @@ import console from './console.mjs'; import github from './github.mjs'; -export default /** @type {const} */ ({ - console, - github, -}); +export default { + console: console, + github: github, +}; diff --git a/src/linter/rules/invalid-change-version.mjs b/src/linter/rules/invalid-change-version.mjs index 34e368f0..d00b92f3 100644 --- a/src/linter/rules/invalid-change-version.mjs +++ b/src/linter/rules/invalid-change-version.mjs @@ -1,3 +1,4 @@ +import { LINT_MESSAGES } from '../../constants.mjs'; import { validateVersion } from '../utils/semver.mjs'; /** @@ -23,7 +24,7 @@ export const invalidChangeVersion = entry => { return invalidVersions.map(version => ({ level: 'warn', - message: `Invalid version number: ${version}`, + message: LINT_MESSAGES.invalidChangeVersion.replace('{{version}}', version), location: { path: entry.api_doc_source, line: entry.yaml_position.start.line, diff --git a/src/linter/rules/missing-introduced-in.mjs b/src/linter/rules/missing-introduced-in.mjs index e2c779db..3c1e339c 100644 --- a/src/linter/rules/missing-introduced-in.mjs +++ b/src/linter/rules/missing-introduced-in.mjs @@ -1,3 +1,5 @@ +import { LINT_MESSAGES } from '../../constants.mjs'; + /** * Checks if `introduced_in` field is missing in the API doc entry. * @@ -13,11 +15,9 @@ export const missingIntroducedIn = entry => { return [ { level: 'info', - message: 'Missing `introduced_in` field in the API doc entry', + message: LINT_MESSAGES.missingIntroducedIn, location: { path: entry.api_doc_source, - // line: entry.yaml_position.start, - // column: entry.yaml_position.end, }, }, ]; From 762adeba7f58ba6835d356d805bc0bb37046e609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Thu, 20 Feb 2025 12:40:31 -0300 Subject: [PATCH 09/22] fix: reporter node position --- src/linter/reporters/console.mjs | 2 +- src/linter/rules/invalid-change-version.mjs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/linter/reporters/console.mjs b/src/linter/reporters/console.mjs index abf26e77..1fdcc7f9 100644 --- a/src/linter/reporters/console.mjs +++ b/src/linter/reporters/console.mjs @@ -16,7 +16,7 @@ const levelToColorMap = { */ export default issue => { const position = issue.location.position - ? ` (${issue.location.position.start}:${issue.location.position.end})` + ? ` (${issue.location.position.start.line}:${issue.location.position.end.line})` : ''; console.log( diff --git a/src/linter/rules/invalid-change-version.mjs b/src/linter/rules/invalid-change-version.mjs index d00b92f3..7be05bd4 100644 --- a/src/linter/rules/invalid-change-version.mjs +++ b/src/linter/rules/invalid-change-version.mjs @@ -27,8 +27,7 @@ export const invalidChangeVersion = entry => { message: LINT_MESSAGES.invalidChangeVersion.replace('{{version}}', version), location: { path: entry.api_doc_source, - line: entry.yaml_position.start.line, - column: entry.yaml_position.start.column, + position: entry.yaml_position, }, })); }; From b7e8597c887b2b407a9f9038afada11b26f353f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Mon, 24 Feb 2025 11:20:23 -0300 Subject: [PATCH 10/22] refactor: some improvements --- bin/cli.mjs | 26 +++++++++--------- src/generators/types.d.ts | 3 --- src/linter/index.mjs | 56 ++++++++++++++++++++++++++------------- 3 files changed, 50 insertions(+), 35 deletions(-) diff --git a/bin/cli.mjs b/bin/cli.mjs index f0c618eb..5091c9d3 100755 --- a/bin/cli.mjs +++ b/bin/cli.mjs @@ -12,7 +12,7 @@ import generators from '../src/generators/index.mjs'; import createMarkdownLoader from '../src/loaders/markdown.mjs'; import createMarkdownParser from '../src/parsers/markdown.mjs'; import createNodeReleases from '../src/releases.mjs'; -import { Linter } from '../src/linter/index.mjs'; +import createLinter from '../src/linter/index.mjs'; import reporters from '../src/linter/reporters/index.mjs'; const availableGenerators = Object.keys(generators); @@ -52,7 +52,9 @@ program 'Set the processing target modes' ).choices(availableGenerators) ) - .addOption(new Option('--skip-linting', 'Skip linting').default(false)) + .addOption( + new Option('--lint-dry-run', 'Run linter in dry-run mode').default(false) + ) .addOption( new Option('-r, --reporter [reporter]', 'Specify the linter reporter') .choices(Object.keys(reporters)) @@ -68,9 +70,9 @@ program * @property {string} output Specifies the directory where output files will be saved. * @property {Target[]} target Specifies the generator target mode. * @property {string} version Specifies the target Node.js version. - * @property {string} changelog Specifies the path to the Node.js CHANGELOG.md file - * @property {boolean} skipLinting Specifies whether to skip linting - * @property {keyof reporters} reporter Specifies the linter reporter + * @property {string} changelog Specifies the path to the Node.js CHANGELOG.md file. + * @property {boolean} lintDryRun Specifies whether the linter should run in dry-run mode. + * @property {keyof reporters} reporter Specifies the linter reporter. * * @name ProgramOptions * @type {Options} @@ -82,11 +84,11 @@ const { target = [], version, changelog, - skipLinting, + lintDryRun, reporter, } = program.opts(); -const linter = skipLinting ? undefined : new Linter(); +const linter = createLinter(lintDryRun); const { loadFiles } = createMarkdownLoader(); const { parseApiDocs } = createMarkdownParser(); @@ -100,7 +102,7 @@ const { runGenerators } = createGenerator(parsedApiDocs); // Retrieves Node.js release metadata from a given Node.js version and CHANGELOG.md file const { getAllMajors } = createNodeReleases(changelog); -linter?.lintAll(parsedApiDocs); +linter.lintAll(parsedApiDocs); await runGenerators({ // A list of target modes for the API docs parser @@ -115,10 +117,8 @@ await runGenerators({ releases: await getAllMajors(), }); -if (linter) { - linter.report(reporter); +linter.report(reporter); - if (linter.hasError) { - exit(1); - } +if (linter.hasError()) { + exit(1); } diff --git a/src/generators/types.d.ts b/src/generators/types.d.ts index e56e84b8..110b5dc0 100644 --- a/src/generators/types.d.ts +++ b/src/generators/types.d.ts @@ -1,7 +1,6 @@ import type { SemVer } from 'semver'; import type availableGenerators from './index.mjs'; import type { ApiDocReleaseEntry } from '../types'; -import type { Linter } from '../linter/index.mjs'; declare global { // All available generators as an inferable type, to allow Generator interfaces @@ -31,8 +30,6 @@ declare global { // A list of all Node.js major versions and their respective release information releases: Array; - - linter: Linter | undefined; } export interface GeneratorMetadata { diff --git a/src/linter/index.mjs b/src/linter/index.mjs index e057e659..37dabc79 100644 --- a/src/linter/index.mjs +++ b/src/linter/index.mjs @@ -7,59 +7,77 @@ import { missingIntroducedIn } from './rules/missing-introduced-in.mjs'; /** * Lint issues in ApiDocMetadataEntry entries + * + * @param {boolean} dryRun Whether to run the linter in dry-run mode */ -export class Linter { +const createLinter = dryRun => { /** * @type {Array} */ - #issues = []; + const issues = []; /** * @type {Array} */ - #rules = [missingIntroducedIn, missingChangeVersion, invalidChangeVersion]; + const rules = [ + missingIntroducedIn, + missingChangeVersion, + invalidChangeVersion, + ]; /** * @param {ApiDocMetadataEntry} entry * @returns {void} */ - lint(entry) { - for (const rule of this.#rules) { - const issues = rule(entry); + const lint = entry => { + for (const rule of rules) { + const ruleIssues = rule(entry); - if (issues.length > 0) { - this.#issues.push(...issues); + if (ruleIssues.length > 0) { + issues.push(...ruleIssues); } } - } + }; /** * @param {ApiDocMetadataEntry[]} entries * @returns {void} */ - lintAll(entries) { + const lintAll = entries => { for (const entry of entries) { - this.lint(entry); + lint(entry); } - } + }; /** * @param {keyof reporters} reporterName */ - report(reporterName) { + const report = reporterName => { + if (dryRun) { + return; + } + const reporter = reporters[reporterName]; - for (const issue of this.#issues) { + for (const issue of issues) { reporter(issue); } - } + }; /** * Returns whether there are any issues with a level of 'error' * * @returns {boolean} */ - get hasError() { - return this.#issues.some(issue => issue.level === 'error'); - } -} + const hasError = () => { + return issues.some(issue => issue.level === 'error'); + }; + + return { + lintAll, + report, + hasError, + }; +}; + +export default createLinter; From 95f67ab7e24c589519981d73addfee31809dffe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Mon, 24 Feb 2025 12:56:30 -0300 Subject: [PATCH 11/22] refactor: better docs --- bin/cli.mjs | 4 +--- src/linter/index.mjs | 17 ++++++++++++++--- src/linter/reporters/console.mjs | 2 ++ src/linter/reporters/github.mjs | 2 +- src/linter/rules/invalid-change-version.mjs | 2 +- src/linter/rules/missing-change-version.mjs | 2 +- src/linter/rules/missing-introduced-in.mjs | 2 +- src/linter/utils/semver.mjs | 1 + 8 files changed, 22 insertions(+), 10 deletions(-) diff --git a/bin/cli.mjs b/bin/cli.mjs index 5091c9d3..57edb16d 100755 --- a/bin/cli.mjs +++ b/bin/cli.mjs @@ -119,6 +119,4 @@ await runGenerators({ linter.report(reporter); -if (linter.hasError()) { - exit(1); -} +exit(Number(linter.hasError())); diff --git a/src/linter/index.mjs b/src/linter/index.mjs index 37dabc79..1b7ed44e 100644 --- a/src/linter/index.mjs +++ b/src/linter/index.mjs @@ -6,17 +6,21 @@ import { missingChangeVersion } from './rules/missing-change-version.mjs'; import { missingIntroducedIn } from './rules/missing-introduced-in.mjs'; /** - * Lint issues in ApiDocMetadataEntry entries + * Creates a linter instance to validate ApiDocMetadataEntry entries * * @param {boolean} dryRun Whether to run the linter in dry-run mode */ const createLinter = dryRun => { /** + * Lint issues found during validations + * * @type {Array} */ const issues = []; /** + * Lint rules to validate the entries against + * * @type {Array} */ const rules = [ @@ -26,6 +30,8 @@ const createLinter = dryRun => { ]; /** + * Validates a ApiDocMetadataEntry entry against all defined rules + * * @param {ApiDocMetadataEntry} entry * @returns {void} */ @@ -40,6 +46,8 @@ const createLinter = dryRun => { }; /** + * Validates an array of ApiDocMetadataEntry entries against all defined rules + * * @param {ApiDocMetadataEntry[]} entries * @returns {void} */ @@ -50,7 +58,10 @@ const createLinter = dryRun => { }; /** - * @param {keyof reporters} reporterName + * Reports found issues using the specified reporter + * + * @param {keyof typeof reporters} reporterName Reporter name + * @returns {void} */ const report = reporterName => { if (dryRun) { @@ -65,7 +76,7 @@ const createLinter = dryRun => { }; /** - * Returns whether there are any issues with a level of 'error' + * Checks if any error-level issues were found during linting * * @returns {boolean} */ diff --git a/src/linter/reporters/console.mjs b/src/linter/reporters/console.mjs index 1fdcc7f9..40a2c6be 100644 --- a/src/linter/reporters/console.mjs +++ b/src/linter/reporters/console.mjs @@ -12,6 +12,8 @@ const levelToColorMap = { }; /** + * Console reporter + * * @type {import('../types.d.ts').Reporter} */ export default issue => { diff --git a/src/linter/reporters/github.mjs b/src/linter/reporters/github.mjs index 1971726a..114a0550 100644 --- a/src/linter/reporters/github.mjs +++ b/src/linter/reporters/github.mjs @@ -9,7 +9,7 @@ const actions = { }; /** - * GitHub action reporter for + * GitHub actions reporter * * @type {import('../types.d.ts').Reporter} */ diff --git a/src/linter/rules/invalid-change-version.mjs b/src/linter/rules/invalid-change-version.mjs index 7be05bd4..00b07baa 100644 --- a/src/linter/rules/invalid-change-version.mjs +++ b/src/linter/rules/invalid-change-version.mjs @@ -2,7 +2,7 @@ import { LINT_MESSAGES } from '../../constants.mjs'; import { validateVersion } from '../utils/semver.mjs'; /** - * Checks if any change version is invalid. + * Checks if any change version is invalid * * @param {ApiDocMetadataEntry} entry * @returns {Array} diff --git a/src/linter/rules/missing-change-version.mjs b/src/linter/rules/missing-change-version.mjs index 67baaa23..c364296f 100644 --- a/src/linter/rules/missing-change-version.mjs +++ b/src/linter/rules/missing-change-version.mjs @@ -1,5 +1,5 @@ /** - * Checks if any change version is missing. + * Checks if any change version is missing * * @param {ApiDocMetadataEntry} entry * @returns {Array} diff --git a/src/linter/rules/missing-introduced-in.mjs b/src/linter/rules/missing-introduced-in.mjs index 3c1e339c..80224c9d 100644 --- a/src/linter/rules/missing-introduced-in.mjs +++ b/src/linter/rules/missing-introduced-in.mjs @@ -1,7 +1,7 @@ import { LINT_MESSAGES } from '../../constants.mjs'; /** - * Checks if `introduced_in` field is missing in the API doc entry. + * Checks if `introduced_in` field is missing * * @param {ApiDocMetadataEntry} entry * @returns {Array} diff --git a/src/linter/utils/semver.mjs b/src/linter/utils/semver.mjs index 24294b34..271d6ede 100644 --- a/src/linter/utils/semver.mjs +++ b/src/linter/utils/semver.mjs @@ -2,6 +2,7 @@ * Validates a semver version string * * @param {string} version + * @returns {boolean} */ export const validateVersion = version => { // https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string From d7394b79b88a44af8ad2860c1d6febca4df359ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Sun, 2 Mar 2025 20:04:30 -0300 Subject: [PATCH 12/22] refactor: remove useless directive --- src/linter/reporters/console.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/linter/reporters/console.mjs b/src/linter/reporters/console.mjs index 40a2c6be..d793c884 100644 --- a/src/linter/reporters/console.mjs +++ b/src/linter/reporters/console.mjs @@ -23,7 +23,6 @@ export default issue => { console.log( styleText( - // @ts-expect-error ForegroundColors is not exported levelToColorMap[issue.level], `${issue.message} at ${issue.location.path}${position}` ) From 622e6a3a14426dc2a7c512c49166c668cac36504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Sun, 2 Mar 2025 20:11:04 -0300 Subject: [PATCH 13/22] refactor: remove semver regex --- src/linter/rules/invalid-change-version.mjs | 4 ++-- src/linter/utils/semver.mjs | 13 ------------- 2 files changed, 2 insertions(+), 15 deletions(-) delete mode 100644 src/linter/utils/semver.mjs diff --git a/src/linter/rules/invalid-change-version.mjs b/src/linter/rules/invalid-change-version.mjs index 00b07baa..779901aa 100644 --- a/src/linter/rules/invalid-change-version.mjs +++ b/src/linter/rules/invalid-change-version.mjs @@ -1,5 +1,5 @@ import { LINT_MESSAGES } from '../../constants.mjs'; -import { validateVersion } from '../utils/semver.mjs'; +import { valid } from 'semver'; /** * Checks if any change version is invalid @@ -19,7 +19,7 @@ export const invalidChangeVersion = entry => { ); const invalidVersions = allVersions.filter( - version => !validateVersion(version.substring(1)) // Trim the leading 'v' from the version string + version => valid(version) === null ); return invalidVersions.map(version => ({ diff --git a/src/linter/utils/semver.mjs b/src/linter/utils/semver.mjs deleted file mode 100644 index 271d6ede..00000000 --- a/src/linter/utils/semver.mjs +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Validates a semver version string - * - * @param {string} version - * @returns {boolean} - */ -export const validateVersion = version => { - // https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string - const regex = - /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/gm; - - return regex.test(version); -}; From a2dae434c20fcc045ab34e763fde75ede6b3b820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Sun, 2 Mar 2025 21:25:05 -0300 Subject: [PATCH 14/22] refactor: add yaml_position description --- src/types.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types.d.ts b/src/types.d.ts index cb89e40a..e0aec952 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -99,6 +99,7 @@ declare global { // Extra YAML section entries that are stringd and serve // to provide additional metadata about the API doc entry tags: Array; + // The postion of the YAML of the API doc entry yaml_position: Position; } From 3e912e7b554c1e16c93329797cff7b2409d8835f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 5 Mar 2025 19:30:33 -0300 Subject: [PATCH 15/22] test: create rules tests --- src/linter/tests/fixtures/entries.mjs | 84 +++++++++++++++++++ .../rules/invalid-change-version.test.mjs | 41 +++++++++ .../rules/missing-change-version.test.mjs | 31 +++++++ .../rules/missing-introduced-in.test.mjs | 38 +++++++++ 4 files changed, 194 insertions(+) create mode 100644 src/linter/tests/fixtures/entries.mjs create mode 100644 src/linter/tests/rules/invalid-change-version.test.mjs create mode 100644 src/linter/tests/rules/missing-change-version.test.mjs create mode 100644 src/linter/tests/rules/missing-introduced-in.test.mjs diff --git a/src/linter/tests/fixtures/entries.mjs b/src/linter/tests/fixtures/entries.mjs new file mode 100644 index 00000000..89f717c9 --- /dev/null +++ b/src/linter/tests/fixtures/entries.mjs @@ -0,0 +1,84 @@ +/** + * Noop function. + * + * @returns {any} + */ +const noop = () => {}; + +/** + * @type {ApiDocMetadataEntry} + */ +export const assertEntry = { + api: 'assert', + slug: 'assert', + source_link: 'lib/assert.js', + api_doc_source: 'doc/api/assert.md', + added_in: undefined, + deprecated_in: undefined, + removed_in: undefined, + n_api_version: undefined, + changes: [ + { + version: 'v9.9.0', + 'pr-url': 'https://github.com/nodejs/node/pull/17615', + description: 'Added error diffs to the strict assertion mode.', + }, + { + version: 'v9.9.0', + 'pr-url': 'https://github.com/nodejs/node/pull/17002', + description: 'Added strict assertion mode to the assert module.', + }, + { + version: ['v13.9.0', 'v12.16.2'], + 'pr-url': 'https://github.com/nodejs/node/pull/31635', + description: + 'Changed "strict mode" to "strict assertion mode" and "legacy mode" to "legacy assertion mode" to avoid confusion with the more usual meaning of "strict mode".', + }, + { + version: 'v15.0.0', + 'pr-url': 'https://github.com/nodejs/node/pull/34001', + description: "Exposed as `require('node:assert/strict')`.", + }, + ], + heading: { + type: 'heading', + depth: 1, + children: [ + { + type: 'text', + value: 'Assert', + position: { + start: { line: 1, column: 3, offset: 2 }, + end: { line: 1, column: 9, offset: 8 }, + }, + }, + ], + position: { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 1, column: 9, offset: 8 }, + }, + data: { + text: 'Assert', + name: 'Assert', + depth: 1, + slug: 'assert', + type: 'property', + }, + toJSON: noop, + }, + stability: { + type: 'root', + children: [], + toJSON: noop, + }, + content: { + type: 'root', + children: [], + }, + tags: [], + introduced_in: 'v0.1.21', + yaml_position: { + start: { line: 7, column: 1, offset: 103 }, + end: { line: 7, column: 35, offset: 137 }, + }, +}; diff --git a/src/linter/tests/rules/invalid-change-version.test.mjs b/src/linter/tests/rules/invalid-change-version.test.mjs new file mode 100644 index 00000000..6f445194 --- /dev/null +++ b/src/linter/tests/rules/invalid-change-version.test.mjs @@ -0,0 +1,41 @@ +import { describe, it } from 'node:test'; +import { invalidChangeVersion } from '../../rules/invalid-change-version.mjs'; +import { deepEqual } from 'node:assert'; +import { assertEntry } from '../fixtures/entries.mjs'; + +describe('invalidChangeVersion', () => { + it('should return an empty array if all change versions are valid', () => { + const issues = invalidChangeVersion(assertEntry); + + deepEqual(issues, []); + }); + + it('should return an issue if a change version is invalid', () => { + const issues = invalidChangeVersion({ + ...assertEntry, + changes: [...assertEntry.changes, { version: ['v13.9.0', 'REPLACEME'] }], + }); + + deepEqual(issues, [ + { + level: 'warn', + location: { + path: 'doc/api/assert.md', + position: { + end: { + column: 35, + line: 7, + offset: 137, + }, + start: { + column: 1, + line: 7, + offset: 103, + }, + }, + }, + message: 'Invalid version number: REPLACEME', + }, + ]); + }); +}); diff --git a/src/linter/tests/rules/missing-change-version.test.mjs b/src/linter/tests/rules/missing-change-version.test.mjs new file mode 100644 index 00000000..ac4fb882 --- /dev/null +++ b/src/linter/tests/rules/missing-change-version.test.mjs @@ -0,0 +1,31 @@ +import { describe, it } from 'node:test'; +import { deepEqual } from 'node:assert'; +import { missingChangeVersion } from '../../rules/missing-change-version.mjs'; +import { assertEntry } from '../fixtures/entries.mjs'; + +describe('missingChangeVersion', () => { + it('should return an empty array if all change versions are non-empty', () => { + const issues = missingChangeVersion(assertEntry); + + deepEqual(issues, []); + }); + + it('should return an issue if a change version is missing', () => { + const issues = missingChangeVersion({ + ...assertEntry, + changes: [...assertEntry.changes, { version: undefined }], + }); + + deepEqual(issues, [ + { + level: 'warn', + location: { + column: 1, + line: 7, + path: 'doc/api/assert.md', + }, + message: 'Missing change version', + }, + ]); + }); +}); diff --git a/src/linter/tests/rules/missing-introduced-in.test.mjs b/src/linter/tests/rules/missing-introduced-in.test.mjs new file mode 100644 index 00000000..f6ca0fe4 --- /dev/null +++ b/src/linter/tests/rules/missing-introduced-in.test.mjs @@ -0,0 +1,38 @@ +import { describe, it } from 'node:test'; +import { missingIntroducedIn } from '../../rules/missing-introduced-in.mjs'; +import { deepEqual } from 'assert'; +import { assertEntry } from '../fixtures/entries.mjs'; + +describe('missingIntroducedIn', () => { + it('should return an empty array if the introduced_in field is not missing', () => { + const issues = missingIntroducedIn(assertEntry); + + deepEqual(issues, []); + }); + + it('should return an empty array if the heading depth is not equal to 1', () => { + const issues = missingIntroducedIn({ + ...assertEntry, + heading: { ...assertEntry.heading, depth: 2 }, + }); + + deepEqual(issues, []); + }); + + it('should return an issue if the introduced_in property is missing', () => { + const issues = missingIntroducedIn({ + ...assertEntry, + introduced_in: undefined, + }); + + deepEqual(issues, [ + { + level: 'info', + location: { + path: 'doc/api/assert.md', + }, + message: "Missing 'introduced_in' field in the API doc entry", + }, + ]); + }); +}); From 7e9a3f4e8f461e4e5d9dad2a685dbc2181d8edba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 5 Mar 2025 20:37:49 -0300 Subject: [PATCH 16/22] test: create reporters tests --- src/linter/reporters/console.mjs | 4 +-- src/linter/tests/fixtures/issues.mjs | 36 +++++++++++++++++++++ src/linter/tests/reporters/console.test.mjs | 26 +++++++++++++++ src/linter/tests/reporters/gihub.test.mjs | 26 +++++++++++++++ 4 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 src/linter/tests/fixtures/issues.mjs create mode 100644 src/linter/tests/reporters/console.test.mjs create mode 100644 src/linter/tests/reporters/gihub.test.mjs diff --git a/src/linter/reporters/console.mjs b/src/linter/reporters/console.mjs index d793c884..58975931 100644 --- a/src/linter/reporters/console.mjs +++ b/src/linter/reporters/console.mjs @@ -21,10 +21,10 @@ export default issue => { ? ` (${issue.location.position.start.line}:${issue.location.position.end.line})` : ''; - console.log( + process.stdout.write( styleText( levelToColorMap[issue.level], `${issue.message} at ${issue.location.path}${position}` - ) + ) + '\n' ); }; diff --git a/src/linter/tests/fixtures/issues.mjs b/src/linter/tests/fixtures/issues.mjs new file mode 100644 index 00000000..1546e8bc --- /dev/null +++ b/src/linter/tests/fixtures/issues.mjs @@ -0,0 +1,36 @@ +// @ts-check + +/** + * @type {import('../../types').LintIssue} + */ +export const infoIssue = { + level: 'info', + location: { + path: 'doc/api/test.md', + }, + 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/reporters/console.test.mjs b/src/linter/tests/reporters/console.test.mjs new file mode 100644 index 00000000..4960c9df --- /dev/null +++ b/src/linter/tests/reporters/console.test.mjs @@ -0,0 +1,26 @@ +import { describe, it } from 'node:test'; +import console from '../../reporters/console.mjs'; +import assert from 'node:assert'; +import { errorIssue, infoIssue, warnIssue } from '../fixtures/issues.mjs'; + +describe('console', () => { + it('should write to stdout with the correct colors based on the issue level', t => { + t.mock.method(process.stdout, 'write'); + + console(infoIssue); + console(warnIssue); + console(errorIssue); + + assert.strictEqual(process.stdout.write.mock.callCount(), 3); + + const callsArgs = process.stdout.write.mock.calls.map( + call => call.arguments[0] + ); + + assert.deepStrictEqual(callsArgs, [ + '\x1B[90mThis is a INFO issue at doc/api/test.md\x1B[39m\n', + '\x1B[33mThis is a WARN issue at doc/api/test.md (1:1)\x1B[39m\n', + '\x1B[31mThis is a ERROR issue at doc/api/test.md (1:1)\x1B[39m\n', + ]); + }); +}); diff --git a/src/linter/tests/reporters/gihub.test.mjs b/src/linter/tests/reporters/gihub.test.mjs new file mode 100644 index 00000000..09e3e4b3 --- /dev/null +++ b/src/linter/tests/reporters/gihub.test.mjs @@ -0,0 +1,26 @@ +import { describe, it } from 'node:test'; +import github from '../../reporters/github.mjs'; +import assert from 'node:assert'; +import { errorIssue, infoIssue, warnIssue } from '../fixtures/issues.mjs'; + +describe('github', () => { + it('should write to stdout with the correct fn based on the issue level', t => { + t.mock.method(process.stdout, 'write'); + + github(infoIssue); + github(warnIssue); + github(errorIssue); + + assert.strictEqual(process.stdout.write.mock.callCount(), 3); + + const callsArgs = process.stdout.write.mock.calls.map( + call => call.arguments[0] + ); + + assert.deepStrictEqual(callsArgs, [ + '::notice file=doc/api/test.md::This is a INFO issue\n', + '::warning file=doc/api/test.md,line=1,endLine=1::This is a WARN issue\n', + '::error file=doc/api/test.md,line=1,endLine=1::This is a ERROR issue\n', + ]); + }); +}); From bcebcddd304283a8289b19deb91fa522c6417c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 5 Mar 2025 20:38:09 -0300 Subject: [PATCH 17/22] fix: missing change version issue location --- src/linter/rules/missing-change-version.mjs | 3 +-- src/linter/tests/rules/missing-change-version.test.mjs | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/linter/rules/missing-change-version.mjs b/src/linter/rules/missing-change-version.mjs index c364296f..67b42a19 100644 --- a/src/linter/rules/missing-change-version.mjs +++ b/src/linter/rules/missing-change-version.mjs @@ -16,8 +16,7 @@ export const missingChangeVersion = entry => { message: 'Missing change version', location: { path: entry.api_doc_source, - line: entry.yaml_position.start.line, - column: entry.yaml_position.start.column, + position: entry.yaml_position, }, })); }; diff --git a/src/linter/tests/rules/missing-change-version.test.mjs b/src/linter/tests/rules/missing-change-version.test.mjs index ac4fb882..bafab7be 100644 --- a/src/linter/tests/rules/missing-change-version.test.mjs +++ b/src/linter/tests/rules/missing-change-version.test.mjs @@ -20,9 +20,11 @@ describe('missingChangeVersion', () => { { level: 'warn', location: { - column: 1, - line: 7, path: 'doc/api/assert.md', + position: { + end: { column: 35, line: 7, offset: 137 }, + start: { column: 1, line: 7, offset: 103 }, + }, }, message: 'Missing change version', }, From 881ce3c385dd6f5d95d3622b3a5347094857effa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 5 Mar 2025 21:06:37 -0300 Subject: [PATCH 18/22] feat: add disable rule cli option --- src/linter/engine.mjs | 51 +++++++++++++++++++++++++++++++ src/linter/index.mjs | 61 ++++++++++++++------------------------ src/linter/rules/index.mjs | 14 +++++++++ 3 files changed, 87 insertions(+), 39 deletions(-) create mode 100644 src/linter/engine.mjs create mode 100644 src/linter/rules/index.mjs diff --git a/src/linter/engine.mjs b/src/linter/engine.mjs new file mode 100644 index 00000000..41d8f3a2 --- /dev/null +++ b/src/linter/engine.mjs @@ -0,0 +1,51 @@ +'use strict'; + +/** + * Creates a linter engine instance to validate ApiDocMetadataEntry entries + * + * @param {import('./types').LintRule} rules Lint rules to validate the entries against + */ +const createLinterEngine = rules => { + /** + * Validates a ApiDocMetadataEntry entry against all defined rules + * + * @param {ApiDocMetadataEntry} entry + * @returns {import('./types').LintIssue[]} + */ + const lint = entry => { + const issues = []; + + for (const rule of rules) { + const ruleIssues = rule(entry); + + if (ruleIssues.length > 0) { + issues.push(...ruleIssues); + } + } + + return issues; + }; + + /** + * Validates an array of ApiDocMetadataEntry entries against all defined rules + * + * @param {ApiDocMetadataEntry[]} entries + * @returns {import('./types').LintIssue[]} + */ + const lintAll = entries => { + const issues = []; + + for (const entry of entries) { + issues.push(...lint(entry)); + } + + return issues; + }; + + return { + lint, + lintAll, + }; +}; + +export default createLinterEngine; diff --git a/src/linter/index.mjs b/src/linter/index.mjs index 1b7ed44e..689c6a63 100644 --- a/src/linter/index.mjs +++ b/src/linter/index.mjs @@ -1,60 +1,32 @@ 'use strict'; +import createLinterEngine from './engine.mjs'; import reporters from './reporters/index.mjs'; -import { invalidChangeVersion } from './rules/invalid-change-version.mjs'; -import { missingChangeVersion } from './rules/missing-change-version.mjs'; -import { missingIntroducedIn } from './rules/missing-introduced-in.mjs'; +import rules from './rules/index.mjs'; /** * Creates a linter instance to validate ApiDocMetadataEntry entries * - * @param {boolean} dryRun Whether to run the linter in dry-run mode + * @param {boolean} dryRun Whether to run the engine in dry-run mode + * @param {string[]} disabledRules List of disabled rules names */ -const createLinter = dryRun => { +const createLinter = (dryRun, disabledRules) => { + const engine = createLinterEngine(getEnabledRules(disabledRules)); + /** * Lint issues found during validations * - * @type {Array} + * @type {Array} */ const issues = []; /** - * Lint rules to validate the entries against - * - * @type {Array} - */ - const rules = [ - missingIntroducedIn, - missingChangeVersion, - invalidChangeVersion, - ]; - - /** - * Validates a ApiDocMetadataEntry entry against all defined rules - * - * @param {ApiDocMetadataEntry} entry - * @returns {void} - */ - const lint = entry => { - for (const rule of rules) { - const ruleIssues = rule(entry); - - if (ruleIssues.length > 0) { - issues.push(...ruleIssues); - } - } - }; - - /** - * Validates an array of ApiDocMetadataEntry entries against all defined rules + * Lints all entries using the linter engine * - * @param {ApiDocMetadataEntry[]} entries - * @returns {void} + * @param entries */ const lintAll = entries => { - for (const entry of entries) { - lint(entry); - } + issues.push(...engine.lintAll(entries)); }; /** @@ -84,6 +56,17 @@ const createLinter = dryRun => { return issues.some(issue => issue.level === 'error'); }; + /** + * Retrieves all enabled rules + * + * @returns {import('./types').LintRule[]} + */ + const getEnabledRules = () => { + return Object.entries(rules) + .filter(([ruleName]) => !disabledRules.includes(ruleName)) + .map(([, rule]) => rule); + }; + return { lintAll, report, diff --git a/src/linter/rules/index.mjs b/src/linter/rules/index.mjs new file mode 100644 index 00000000..b780eca9 --- /dev/null +++ b/src/linter/rules/index.mjs @@ -0,0 +1,14 @@ +'use strict'; + +import { invalidChangeVersion } from './invalid-change-version.mjs'; +import { missingChangeVersion } from './missing-change-version.mjs'; +import { missingIntroducedIn } from './missing-introduced-in.mjs'; + +/** + * @type {Record} + */ +export default { + 'invalid-change-version': invalidChangeVersion, + 'missing-change-version': missingChangeVersion, + 'missing-introduced-in': missingIntroducedIn, +}; From 5aeccfe6e78bf53b9b6f90bf0ac38a94d2a23137 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 5 Mar 2025 21:20:16 -0300 Subject: [PATCH 19/22] test: create engine test --- src/linter/tests/engine.test.mjs | 44 ++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/linter/tests/engine.test.mjs diff --git a/src/linter/tests/engine.test.mjs b/src/linter/tests/engine.test.mjs new file mode 100644 index 00000000..f80d1b7e --- /dev/null +++ b/src/linter/tests/engine.test.mjs @@ -0,0 +1,44 @@ +import { describe, mock, it } from 'node:test'; +import assert from 'node:assert/strict'; +import createLinterEngine from '../engine.mjs'; +import { assertEntry } from './fixtures/entries.mjs'; +import { errorIssue, infoIssue, warnIssue } from './fixtures/issues.mjs'; + +describe('createLinterEngine', () => { + it('should call each rule with the provided entry', () => { + const rule1 = mock.fn(() => []); + const rule2 = mock.fn(() => []); + + const engine = createLinterEngine([rule1, rule2]); + + engine.lint(assertEntry); + + assert.strictEqual(rule1.mock.callCount(), 1); + assert.strictEqual(rule2.mock.callCount(), 1); + + assert.deepEqual(rule1.mock.calls[0].arguments, [assertEntry]); + assert.deepEqual(rule2.mock.calls[0].arguments, [assertEntry]); + }); + + it('should return the aggregated issues from all rules', () => { + const rule1 = mock.fn(() => [infoIssue, warnIssue]); + const rule2 = mock.fn(() => [errorIssue]); + + const engine = createLinterEngine([rule1, rule2]); + + const issues = engine.lint(assertEntry); + + assert.equal(issues.length, 2); + assert.deepEqual(issues, [infoIssue, warnIssue, errorIssue]); + }); + + it('should return an empty array when no issues are found', () => { + const rule = () => []; + + const engine = createLinterEngine([rule]); + + const issues = engine.lint(assertEntry); + + assert.deepEqual(issues, []); + }); +}); From bf4d9355b363097c900a68139ae2732d89aa9435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 5 Mar 2025 21:20:36 -0300 Subject: [PATCH 20/22] feat: create cli option --- bin/cli.mjs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/bin/cli.mjs b/bin/cli.mjs index 57edb16d..1d0620d8 100755 --- a/bin/cli.mjs +++ b/bin/cli.mjs @@ -14,6 +14,7 @@ import createMarkdownParser from '../src/parsers/markdown.mjs'; import createNodeReleases from '../src/releases.mjs'; import createLinter from '../src/linter/index.mjs'; import reporters from '../src/linter/reporters/index.mjs'; +import rules from '../src/linter/rules/index.mjs'; const availableGenerators = Object.keys(generators); @@ -52,6 +53,11 @@ program 'Set the processing target modes' ).choices(availableGenerators) ) + .addOption( + new Option('--disable-rule [rule...]', 'Disable a specific linter rule') + .choices(Object.keys(rules)) + .default([]) + ) .addOption( new Option('--lint-dry-run', 'Run linter in dry-run mode').default(false) ) @@ -71,6 +77,7 @@ program * @property {Target[]} target Specifies the generator target mode. * @property {string} version Specifies the target Node.js version. * @property {string} changelog Specifies the path to the Node.js CHANGELOG.md file. + * @property {string[]} disableRule Specifies the linter rules to disable. * @property {boolean} lintDryRun Specifies whether the linter should run in dry-run mode. * @property {keyof reporters} reporter Specifies the linter reporter. * @@ -84,11 +91,12 @@ const { target = [], version, changelog, + disableRule, lintDryRun, reporter, } = program.opts(); -const linter = createLinter(lintDryRun); +const linter = createLinter(lintDryRun, disableRule); const { loadFiles } = createMarkdownLoader(); const { parseApiDocs } = createMarkdownParser(); From fab1c6a5bcc723ee52ffccc7eb805f7e2d496572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 5 Mar 2025 21:23:29 -0300 Subject: [PATCH 21/22] test: oops --- src/linter/tests/engine.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linter/tests/engine.test.mjs b/src/linter/tests/engine.test.mjs index f80d1b7e..cbbd1c5a 100644 --- a/src/linter/tests/engine.test.mjs +++ b/src/linter/tests/engine.test.mjs @@ -28,7 +28,7 @@ describe('createLinterEngine', () => { const issues = engine.lint(assertEntry); - assert.equal(issues.length, 2); + assert.equal(issues.length, 3); assert.deepEqual(issues, [infoIssue, warnIssue, errorIssue]); }); From b1fa0a15dc2c53202dfd0e13cfc2513bd12dafa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Thu, 6 Mar 2025 14:26:43 -0300 Subject: [PATCH 22/22] chore: add FORCE_COLOR env --- .github/workflows/pr.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 1f2f2c4a..ccab0879 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -9,6 +9,9 @@ on: permissions: contents: read +env: + FORCE_COLOR: 1 + jobs: build: runs-on: ubuntu-latest