Skip to content

Commit 33e0f6e

Browse files
committed
feat: add jsdoc-example-eslint ESLint rule
Add a new custom ESLint rule that extracts JavaScript code from JSDoc `@example` blocks and lints it using ESLint's synchronous `Linter` API. The rule is a pure mechanism: it accepts a `rules` object and lints example code with those rules, reporting violations mapped back to the correct source file lines. The rule is configured in `.eslintrc.overrides.js`, where the examples ESLint config is loaded (avoiding a circular dependency with `stdlib.js`), filtered to exclude plugin rules and rules incompatible with code snippets, and passed to the rule. --- type: pre_commit_static_analysis_report description: Results of running static analysis checks when committing changes. report: - task: lint_filenames status: passed - task: lint_editorconfig status: passed - task: lint_markdown status: passed - task: lint_package_json status: passed - task: lint_repl_help status: na - task: lint_javascript_src status: passed - task: lint_javascript_cli status: na - task: lint_javascript_examples status: passed - task: lint_javascript_tests status: passed - task: lint_javascript_benchmarks status: na - task: lint_python status: na - task: lint_r status: na - task: lint_c_src status: na - task: lint_c_examples status: na - task: lint_c_benchmarks status: na - task: lint_c_tests_fixtures status: na - task: lint_shell status: na - task: lint_typescript_declarations status: passed - task: lint_typescript_tests status: na - task: lint_license_headers status: passed ---
1 parent d02089a commit 33e0f6e

File tree

12 files changed

+975
-1
lines changed

12 files changed

+975
-1
lines changed

etc/eslint/.eslintrc.overrides.js

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,72 @@
1616
* limitations under the License.
1717
*/
1818

19+
/* eslint-disable vars-on-top, stdlib/empty-line-before-comment */
20+
1921
'use strict';
2022

2123
// MODULES //
2224

2325
// FIXME: remove the next line and uncomment the subsequent line once all remark JSDoc ESLint rules are completed
2426
var copy = require( './../../lib/node_modules/@stdlib/utils/copy' );
25-
2627
// var copy = require( './utils/copy.js' );
28+
var objectKeys = require( './../../lib/node_modules/@stdlib/utils/keys' );
2729
var defaults = require( './.eslintrc.js' );
30+
var examplesConfig = require( './.eslintrc.examples.js' );
31+
32+
33+
// VARIABLES //
34+
35+
/**
36+
* List of rules to exclude when linting JSDoc code snippets because they are incompatible with code fragments extracted from JSDoc comments.
37+
*
38+
* @private
39+
* @type {Array}
40+
*/
41+
var JSDOC_SNIPPET_EXCLUDE = [
42+
'no-undef',
43+
'no-unused-vars',
44+
'strict',
45+
'no-var',
46+
'eol-last',
47+
'indent',
48+
'no-restricted-syntax'
49+
];
50+
51+
52+
// FUNCTIONS //
53+
54+
/**
55+
* Extracts built-in ESLint rules from a configuration object, filtering out plugin rules and rules listed in an exclusion list.
56+
*
57+
* @private
58+
* @param {Object} config - ESLint configuration object
59+
* @param {Array} exclude - rule names to exclude
60+
* @returns {Object} filtered rules object
61+
*/
62+
function extractSnippetRules( config, exclude ) {
63+
var rules;
64+
var keys;
65+
var key;
66+
var i;
67+
68+
rules = {};
69+
keys = objectKeys( config.rules );
70+
for ( i = 0; i < keys.length; i++ ) {
71+
key = keys[ i ];
72+
73+
// Skip plugin rules (e.g., "stdlib/foo", "node/bar") since the Linter instance does not have them registered:
74+
if ( key.indexOf( '/' ) !== -1 ) {
75+
continue;
76+
}
77+
// Skip rules explicitly excluded as incompatible with code snippets:
78+
if ( exclude.indexOf( key ) !== -1 ) {
79+
continue;
80+
}
81+
rules[ key ] = config.rules[ key ];
82+
}
83+
return rules;
84+
}
2885

2986

3087
// MAIN //
@@ -45,6 +102,13 @@ var eslint = copy( defaults );
45102
*/
46103
eslint.overrides = require( './overrides' );
47104

105+
/**
106+
* Configure JSDoc example linting using the examples ESLint config, with rules filtered for use on JSDoc code snippets.
107+
*/
108+
eslint.rules[ 'stdlib/jsdoc-example-eslint' ] = [ 'warn', {
109+
'rules': extractSnippetRules( examplesConfig, JSDOC_SNIPPET_EXCLUDE )
110+
}];
111+
48112

49113
// EXPORTS //
50114

etc/eslint/rules/stdlib.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,38 @@ rules[ 'stdlib/jsdoc-emphasis-marker' ] = [ 'error', '_' ];
867867
*/
868868
rules[ 'stdlib/jsdoc-empty-line-before-example' ] = 'error';
869869

870+
/**
871+
* Lint JavaScript code in JSDoc example blocks using ESLint.
872+
*
873+
* @name jsdoc-example-eslint
874+
* @memberof rules
875+
* @type {string}
876+
* @default 'warn'
877+
*
878+
* @example
879+
* // Bad...
880+
*
881+
* /**
882+
* * Squares a number.
883+
* *
884+
* * @example
885+
* * var y = square( 3.0 )
886+
* *\/
887+
* function square( x ) {}
888+
*
889+
* @example
890+
* // Good...
891+
*
892+
* /**
893+
* * Squares a number.
894+
* *
895+
* * @example
896+
* * var y = square( 3.0 );
897+
* *\/
898+
* function square( x ) {}
899+
*/
900+
rules[ 'stdlib/jsdoc-example-eslint' ] = 'off';
901+
870902
/**
871903
* Enforce empty lines between requires and code in JSDoc examples.
872904
*
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
<!--
2+
3+
@license Apache-2.0
4+
5+
Copyright (c) 2026 The Stdlib Authors.
6+
7+
Licensed under the Apache License, Version 2.0 (the "License");
8+
you may not use this file except in compliance with the License.
9+
You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.
18+
19+
-->
20+
21+
# jsdoc-example-eslint
22+
23+
> [ESLint rule][eslint-rules] to lint JavaScript code in JSDoc example blocks.
24+
25+
<section class="intro">
26+
27+
</section>
28+
29+
<!-- /.intro -->
30+
31+
<section class="usage">
32+
33+
## Usage
34+
35+
```javascript
36+
var rule = require( '@stdlib/_tools/eslint/rules/jsdoc-example-eslint' );
37+
```
38+
39+
#### rule
40+
41+
[ESLint rule][eslint-rules] to lint JavaScript code in JSDoc example blocks. The rule extracts code from `@example` tags in JSDoc comments, lints it using a provided set of ESLint rules, and reports violations mapped back to the original source lines.
42+
43+
The rule accepts an options object with a `rules` property specifying which ESLint rules to apply to the extracted example code.
44+
45+
**Bad** (missing semicolon):
46+
47+
<!-- eslint stdlib/jsdoc-example-eslint: "off", stdlib/jsdoc-doctest-marker: "off", valid-jsdoc: "off" -->
48+
49+
```javascript
50+
/**
51+
* Squares a number.
52+
*
53+
* @example
54+
* var y = square( 3.0 )
55+
*/
56+
function square( x ) {
57+
return x * x;
58+
}
59+
```
60+
61+
**Good**:
62+
63+
<!-- eslint valid-jsdoc: "off" -->
64+
65+
```javascript
66+
/**
67+
* Squares a number.
68+
*
69+
* @example
70+
* var y = square( 3.0 );
71+
*/
72+
function square( x ) {
73+
return x * x;
74+
}
75+
```
76+
77+
</section>
78+
79+
<!-- /.usage -->
80+
81+
<section class="examples">
82+
83+
## Examples
84+
85+
<!-- eslint no-undef: "error" -->
86+
87+
```javascript
88+
var Linter = require( 'eslint' ).Linter;
89+
var rule = require( '@stdlib/_tools/eslint/rules/jsdoc-example-eslint' );
90+
91+
var linter = new Linter();
92+
var result;
93+
var code;
94+
95+
code = [
96+
'/**',
97+
'* Squares a number.',
98+
'*',
99+
'* @example',
100+
'* var y = square( 3.0 )',
101+
'* // returns 9.0',
102+
'*/',
103+
'function square( x ) {',
104+
'\treturn x * x;',
105+
'}'
106+
].join( '\n' );
107+
108+
linter.defineRule( 'jsdoc-example-eslint', rule );
109+
110+
result = linter.verify( code, {
111+
'rules': {
112+
'jsdoc-example-eslint': [ 'error', {
113+
'rules': {
114+
'semi': 'error'
115+
}
116+
}]
117+
}
118+
});
119+
console.log( result );
120+
/* =>
121+
[
122+
{
123+
'ruleId': 'jsdoc-example-eslint',
124+
'severity': 2,
125+
'message': 'Missing semicolon. (semi)',
126+
...
127+
}
128+
]
129+
*/
130+
```
131+
132+
</section>
133+
134+
<!-- /.examples -->
135+
136+
<!-- Section for related `stdlib` packages. Do not manually edit this section, as it is automatically populated. -->
137+
138+
<section class="related">
139+
140+
</section>
141+
142+
<!-- /.related -->
143+
144+
<!-- Section for all links. Make sure to keep an empty line after the `section` element and another before the `/section` close. -->
145+
146+
<section class="links">
147+
148+
[eslint-rules]: https://eslint.org/docs/developer-guide/working-with-rules
149+
150+
</section>
151+
152+
<!-- /.links -->
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* @license Apache-2.0
3+
*
4+
* Copyright (c) 2026 The Stdlib Authors.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
'use strict';
20+
21+
var Linter = require( 'eslint' ).Linter;
22+
var rule = require( './../lib' );
23+
24+
var linter = new Linter();
25+
26+
// Valid example with proper style:
27+
var valid = [
28+
'/**',
29+
'* Squares a number.',
30+
'*',
31+
'* @example',
32+
'* var y = square( 3.0 );',
33+
'* // returns 9.0',
34+
'*/',
35+
'function square( x ) {',
36+
'\treturn x * x;',
37+
'}'
38+
].join( '\n' );
39+
40+
// Invalid example with missing semicolon:
41+
var invalid = [
42+
'/**',
43+
'* Squares a number.',
44+
'*',
45+
'* @example',
46+
'* var y = square( 3.0 )',
47+
'* // returns 9.0',
48+
'*/',
49+
'function square( x ) {',
50+
'\treturn x * x;',
51+
'}'
52+
].join( '\n' );
53+
54+
// Register the rule:
55+
linter.defineRule( 'jsdoc-example-eslint', rule );
56+
57+
// Lint the valid example:
58+
var validResult = linter.verify( valid, {
59+
'rules': {
60+
'jsdoc-example-eslint': [ 'error', {
61+
'rules': {
62+
'semi': 'error'
63+
}
64+
}]
65+
}
66+
});
67+
console.log( 'Valid example - Number of errors: %d', validResult.length );
68+
69+
// Lint the invalid example:
70+
var invalidResult = linter.verify( invalid, {
71+
'rules': {
72+
'jsdoc-example-eslint': [ 'error', {
73+
'rules': {
74+
'semi': 'error'
75+
}
76+
}]
77+
}
78+
});
79+
console.log( 'Invalid example - Number of errors: %d', invalidResult.length );
80+
if ( invalidResult.length > 0 ) {
81+
console.log( 'Error message: %s', invalidResult[ 0 ].message );
82+
}

0 commit comments

Comments
 (0)