Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d309313
feat: add `validate-jsdoc-codeblocks` lint rule
som-sm Oct 10, 2025
f999e3f
fix: JSDoc example
som-sm Oct 10, 2025
4e1f384
fix: add target in lint rule compiler options
som-sm Oct 10, 2025
defc628
fix: JSDoc example
som-sm Oct 10, 2025
308b9ef
fix: JSDoc example
som-sm Oct 10, 2025
ebbe6d2
perf: use env.updateFile API
som-sm Oct 10, 2025
a08ecd5
chore: merge branch 'main'
som-sm Nov 4, 2025
d2e6aab
refactor: ignore internal types
som-sm Nov 4, 2025
bf7296a
fix: JSDoc example
som-sm Nov 4, 2025
81bb96b
fix: ignore `internal` dir within `source`
som-sm Nov 4, 2025
f7b5e8a
fix: JSDoc example
som-sm Nov 6, 2025
484ee71
perf: create virtual environment once per test run
som-sm Nov 6, 2025
f2d140d
test: add valid cases
som-sm Nov 7, 2025
8aecd47
test: add realworld invalid cases
som-sm Nov 7, 2025
6752782
style: improve naming
som-sm Nov 7, 2025
29e5d5c
refactor: improve indentation within interpolated values
som-sm Nov 7, 2025
881cc94
refactor: improve JSDoc creation test utils
som-sm Nov 8, 2025
4636f02
chore: improve comment text
som-sm Nov 8, 2025
fa2b072
refactor: improve `errorAt` API
som-sm Nov 8, 2025
a992623
test: add more cases
som-sm Nov 8, 2025
f17b794
refactor: simplify `codeWithErrors` code sample
som-sm Nov 8, 2025
c5bdfd6
test: add basic invalid cases
som-sm Nov 8, 2025
0f911b8
chore: fix unnecessary space
som-sm Nov 8, 2025
a9d54fc
refactor: remove abstraction and inline the `code` and `errors`
som-sm Nov 8, 2025
06a1472
test: inline the `code` for remaining invalid cases
som-sm Nov 9, 2025
5332d37
refactor: simplify `errorAt` util
som-sm Nov 9, 2025
af70cac
test: use `errorAt` for `errors`
som-sm Nov 9, 2025
756d986
test: improve cases
som-sm Nov 9, 2025
dea3ea0
refactor: move lint rule to already existing config block
som-sm Nov 10, 2025
3c096b2
style: improve naming
som-sm Nov 10, 2025
6f158c6
test: add more cases
som-sm Nov 10, 2025
606d050
test: move doc creation utils to `test-utils`
som-sm Nov 10, 2025
a8d7286
fix: position of internal files early exit
som-sm Nov 10, 2025
8480dcf
fix: `IsNever` JSDoc example
som-sm Nov 10, 2025
d9e1a1b
fix: revert changes to JSDoc examples of internal types
som-sm Nov 10, 2025
b3b5b9c
refactor: disable `no-irregular-whitespace` only for comments
som-sm Nov 10, 2025
697f197
Update split.d.ts
sindresorhus Nov 12, 2025
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
3 changes: 3 additions & 0 deletions lint-rules/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import os from 'node:os';
import path from 'node:path';
import {RuleTester} from 'eslint';
import tsParser from '@typescript-eslint/parser';
import dedent from 'dedent';

export const createRuleTester = (overrides = {}) => {
const {
Expand Down Expand Up @@ -98,3 +99,5 @@ export const createTypeAwareRuleTester = (fixtureFiles, options = {}) => {
writeFixture,
};
};

export const dedenter = dedent.withOptions({alignValues: true});
116 changes: 116 additions & 0 deletions lint-rules/validate-jsdoc-codeblocks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import path from 'node:path';
import ts from 'typescript';
import {createFSBackedSystem, createVirtualTypeScriptEnvironment} from '@typescript/vfs';

const CODEBLOCK_REGEX = /(?<openingFence>```(?:ts|typescript)?\n)(?<code>[\s\S]*?)```/g;
const FILENAME = 'example-codeblock.ts';

const compilerOptions = {
lib: ['lib.es2023.d.ts', 'lib.dom.d.ts', 'lib.dom.iterable.d.ts'],
target: ts.ScriptTarget.ESNext,
module: ts.ModuleKind.Node20,
moduleResolution: ts.ModuleResolutionKind.Node16,
strict: true,
noImplicitReturns: true,
noImplicitOverride: true,
noUnusedLocals: false, // This is intentionally disabled
noUnusedParameters: true,
noFallthroughCasesInSwitch: true,
noUncheckedIndexedAccess: true,
noPropertyAccessFromIndexSignature: true,
noUncheckedSideEffectImports: true,
useDefineForClassFields: true,
exactOptionalPropertyTypes: true,
};

const virtualFsMap = new Map();
virtualFsMap.set(FILENAME, '// Can\'t be empty');

const rootDir = path.join(import.meta.dirname, '..');
const system = createFSBackedSystem(virtualFsMap, rootDir, ts);
const env = createVirtualTypeScriptEnvironment(system, [FILENAME], ts, compilerOptions);

export const validateJSDocCodeblocksRule = /** @type {const} */ ({
meta: {
type: 'suggestion',
docs: {
description: 'Ensures JSDoc example codeblocks don\'t have errors',
},
messages: {
error: '{{message}}',
},
schema: [],
},
defaultOptions: [],
create(context) {
return {
TSTypeAliasDeclaration(node) {
const filename = context.filename.replaceAll('\\', '/');

// Skip internal files
if (filename.includes('/internal/')) {
return {};
}

const {parent} = node;

// Skip if type is not exported or starts with an underscore (private/internal)
if (parent.type !== 'ExportNamedDeclaration' || node.id.name.startsWith('_')) {
return;
}

const previousNodes = [context.sourceCode.getTokenBefore(parent, {includeComments: true})];

// Handle JSDoc blocks for options
if (node.id.name.endsWith('Options') && node.typeAnnotation.type === 'TSTypeLiteral') {
for (const member of node.typeAnnotation.members) {
previousNodes.push(context.sourceCode.getTokenBefore(member, {includeComments: true}));
}
}

for (const previousNode of previousNodes) {
// Skip if previous node is not a JSDoc comment
if (!previousNode || previousNode.type !== 'Block' || !previousNode.value.startsWith('*')) {
continue;
}

const comment = previousNode.value;

for (const match of comment.matchAll(CODEBLOCK_REGEX)) {
const {code, openingFence} = match.groups ?? {};

// Skip empty code blocks
if (!code || !openingFence) {
continue;
}

const matchOffset = match.index + openingFence.length + 2; // Add `2` because `comment` doesn't include the starting `/*`
const codeStartIndex = previousNode.range[0] + matchOffset;

env.updateFile(FILENAME, code);
const syntacticDiagnostics = env.languageService.getSyntacticDiagnostics(FILENAME);
const semanticDiagnostics = env.languageService.getSemanticDiagnostics(FILENAME);
const diagnostics = syntacticDiagnostics.length > 0 ? syntacticDiagnostics : semanticDiagnostics; // Show semantic errors only if there are no syntactic errors

for (const diagnostic of diagnostics) {
// If diagnostic location is not available, report on the entire code block
const diagnosticStart = codeStartIndex + (diagnostic.start ?? 0);
const diagnosticEnd = diagnosticStart + (diagnostic.length ?? code.length);

context.report({
loc: {
start: context.sourceCode.getLocFromIndex(diagnosticStart),
end: context.sourceCode.getLocFromIndex(diagnosticEnd),
},
messageId: 'error',
data: {
message: ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
},
});
}
}
}
},
};
},
});
Loading