Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/remark-lint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,22 @@ Metadata can be provided in comments:
<!-- llm_description= Utilities for working with file paths -->
```

### `node-core:invalid-type-reference`

Ensures that all `{type}` references are valid types and formatted correctly.

**Allowed:**

```markdown
This is usually a {boolean}, but it could also be a {string|number}.
```

**Not allowed:**

```markdown
This is an {invalid} type, and so is {string | number} because there should **not** be whitespace around the `|`.
```

### `node-core:yaml-comments`

Enforces structure and content of YAML comment blocks:
Expand Down
3 changes: 2 additions & 1 deletion packages/remark-lint/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@node-core/remark-lint",
"type": "module",
"version": "1.0.0",
"version": "1.1.0",
"exports": {
".": "./src/index.mjs",
"./api": "./src/api.mjs"
Expand All @@ -20,6 +20,7 @@
"test:unit": "cross-env NODE_NO_WARNINGS=1 node --experimental-test-coverage --test \"**/*.test.mjs\""
},
"dependencies": {
"@nodejs/doc-kit": "github:nodejs/doc-kit",
"remark-gfm": "^4.0.1",
"remark-lint-blockquote-indentation": "^4.0.1",
"remark-lint-checkbox-character-style": "^5.0.1",
Expand Down
3 changes: 3 additions & 0 deletions packages/remark-lint/src/api.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import remarkLintUnorderedListMarkerStyle from 'remark-lint-unordered-list-marke
import basePreset from './index.mjs';
import duplicateStabilityNodes from './rules/duplicate-stability-nodes.mjs';
import hashedSelfReference from './rules/hashed-self-reference.mjs';
import invalidTypeReference from './rules/invalid-type-reference.mjs';
import orderedReferences from './rules/ordered-references.mjs';
import requiredMetadata from './rules/required-metadata.mjs';
import yamlComments from './rules/yaml/index.mjs';
Expand Down Expand Up @@ -34,6 +35,7 @@ export default (options = {}) => ({
hashedSelfReference,
orderedReferences,
requiredMetadata,
invalidTypeReference,
].map(plugin => [plugin, options]),

// External Rules
Expand Down Expand Up @@ -61,6 +63,7 @@ export default (options = {}) => ({
{ yes: 'Unix' },
{ yes: 'Valgrind' },
{ yes: 'V8' },
{ yes: 'npm' },
],
],
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, it } from 'node:test';

import { testRule } from './utils.mjs';
import invalidTypeReference from '../invalid-type-reference.mjs';

const testCases = [
{
name: 'no references',
input: 'Just some text.',
expected: [],
},
{
name: 'single reference',
input: 'Just a {number}.',
expected: [],
},
{
name: 'miswrapped reference',
input: 'First a {string}, then a \\<number>.',
expected: ['Type reference must be wrapped in "{}"; saw "<number>"'],
},
{
name: 'multiple references',
input: 'Psst, are you a {string | boolean}',
expected: [
'Type reference should be separated by "|", without spaces; saw "{string | boolean}"',
],
},
{
name: 'invalid references',
input: 'This is {invalid}.',
expected: ['Invalid type reference: {invalid}'],
},
];

describe('invalid-type-reference', () => {
for (const { name, input, expected } of testCases) {
it(name, () => testRule(invalidTypeReference, input, expected));
}
});
60 changes: 25 additions & 35 deletions packages/remark-lint/src/rules/duplicate-stability-nodes.mjs
Original file line number Diff line number Diff line change
@@ -1,53 +1,43 @@
import createQueries from '@nodejs/doc-kit/src/utils/queries/index.mjs';
import { lintRule } from 'unified-lint-rule';
import { visit } from 'unist-util-visit';

// TODO(@avivkeller): This is re-used from doc-kit
// Regex to match "Stability: <number>" in blockquotes
const STABILITY = /Stability: ([0-5](?:\.[0-3])?)/;

/**
* Finds and reports duplicate stability nodes
* @type {import('unified-lint-rule').Rule}
*/
const duplicateStabilityNodes = (tree, vfile) => {
let currentDepth = 0;
let currentStability = -1;
let currentHeaderDepth = 0;
// Map depth → stability string recorded at that depth
const stabilityByDepth = new Map();
let currentHeadingDepth = 0; // Current heading depth (0 for "no heading")

visit(tree, node => {
// Update the current heading depth whenever a heading node is encountered
visit(tree, ['heading', 'blockquote'], node => {
if (node.type === 'heading') {
currentHeaderDepth = node.depth;
// Update heading depth and clear deeper recorded stabilities
currentHeadingDepth = node.depth;
for (const depth of stabilityByDepth.keys()) {
if (depth >= currentHeadingDepth) stabilityByDepth.delete(depth);
}
return;
}

// Look for blockquotes which may contain stability indicators
if (node.type === 'blockquote') {
// Assume the first child is a paragraph
const paragraph = node.children?.[0];
// And the first child of that paragraph is text
const text = paragraph?.children?.[0];
// Handle blockquotes: extract text from paragraph > text structure
const text = node.children?.[0]?.children?.[0]?.value;
if (!text) return;

// Ensure structure is paragraph > text
if (paragraph?.type === 'paragraph' && text?.type === 'text') {
// Try to match "Stability: X"
const match = text.value.match(STABILITY);
if (match) {
const stability = parseFloat(match[1]);
// If the heading got deeper, and stability is valid and matches previous, report a duplicate
if (
currentHeaderDepth > currentDepth &&
stability >= 0 &&
stability === currentStability
) {
vfile.message('Duplicate stability node', node);
} else {
// Otherwise, record this stability and heading depth
currentDepth = currentHeaderDepth;
currentStability = stability;
}
}
const match = createQueries.QUERIES.stabilityIndexPrefix.exec(text); // Match "Stability: X"
if (!match) return;

const stability = match[1];
// Report if a duplicate stability exists in a parent heading depth
for (const [depth, prevStability] of stabilityByDepth) {
if (depth < currentHeadingDepth && prevStability === stability) {
vfile.message('Duplicate stability node', node);
break;
}
}

stabilityByDepth.set(currentHeadingDepth, stability);
});
};

Expand Down
1 change: 1 addition & 0 deletions packages/remark-lint/src/rules/hashed-self-reference.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const hashedSelfReference = (tree, vfile) => {

if (targetURL.pathname === currentFileURL.pathname) {
const expected = url.includes('#') ? url.slice(url.indexOf('#')) : '#';
node.url = expected;

vfile.message(
`Self-reference must start with hash (expected "${expected}", got "${url}")`,
Expand Down
49 changes: 49 additions & 0 deletions packages/remark-lint/src/rules/invalid-type-reference.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { transformTypeToReferenceLink } from '@nodejs/doc-kit/src/utils/parser/index.mjs';
import createQueries from '@nodejs/doc-kit/src/utils/queries/index.mjs';
import { lintRule } from 'unified-lint-rule';
import { visit } from 'unist-util-visit';

const MATCH_RE = /\s\||\|\s/g;
const REPLACE_RE = /\s*\|\s*/g;

/**
* Ensures that all type references are valid
* @type {import('unified-lint-rule').Rule}
*/
const invalidTypeReference = (tree, vfile) => {
visit(tree, createQueries.UNIST.isTextWithType, node => {
const types = node.value.match(createQueries.QUERIES.normalizeTypes);

types.forEach(type => {
// Ensure wrapped in {}
if (type[0] !== '{' || type[type.length - 1] !== '}') {
vfile.message(
`Type reference must be wrapped in "{}"; saw "${type}"`,
node
);

node.value = node.value.replace(type, `{${type.slice(1, -1)}}`);
}

// Fix spaces around |
if (MATCH_RE.test(type)) {
vfile.message(
`Type reference should be separated by "|", without spaces; saw "${type}"`,
node
);

const normalized = type.replace(REPLACE_RE, '|');
node.value = node.value.replace(type, normalized);
}

if (transformTypeToReferenceLink(type) === type) {
vfile.message(`Invalid type reference: ${type}`, node);
}
});
});
};

export default lintRule(
'node-core:invalid-type-reference',
invalidTypeReference
);
Loading
Loading