Skip to content

Commit ad82cb8

Browse files
Planeshifterkgrytestdlib-bot
authored
build: add tsdoc-doctest ESLint rule
PR-URL: stdlib-js#8039 Closes: stdlib-js/metr-issue-tracker#48 Co-authored-by: Athan Reines <[email protected]> Reviewed-by: Athan Reines <[email protected]> Co-authored-by: stdlib-bot <[email protected]>
1 parent 2ee3e58 commit ad82cb8

File tree

32 files changed

+2516
-1
lines changed

32 files changed

+2516
-1
lines changed

etc/eslint/plugins/typescript.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ var plugins = [
3030
'eslint-plugin-jsdoc',
3131

3232
// Required for TypeScript support:
33-
'@typescript-eslint'
33+
'@typescript-eslint',
34+
35+
// Custom stdlib rules:
36+
'stdlib'
3437
];
3538

3639

etc/eslint/rules/typescript.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2712,6 +2712,17 @@ rules[ 'yoda' ] = 'error';
27122712
*/
27132713
rules[ 'expect-type/expect' ] = 'error';
27142714

2715+
/**
2716+
* Ensures return annotations in TSDoc examples match the actual output.
2717+
*
2718+
* @name stdlib/tsdoc-declarations-doctest
2719+
* @memberof rules
2720+
* @type {string}
2721+
* @default 'error'
2722+
* @see {@link module:@stdlib/_tools/eslint/rules/tsdoc-declarations-doctest}
2723+
*/
2724+
rules[ 'stdlib/tsdoc-declarations-doctest' ] = 'error';
2725+
27152726

27162727
// EXPORTS //
27172728

lib/node_modules/@stdlib/_tools/eslint/rules/lib/index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,6 +1071,15 @@ setReadOnly( rules, 'section-headers', require( '@stdlib/_tools/eslint/rules/sec
10711071
*/
10721072
setReadOnly( rules, 'ternary-condition-parentheses', require( '@stdlib/_tools/eslint/rules/ternary-condition-parentheses' ) );
10731073

1074+
/**
1075+
* @name tsdoc-declarations-doctest
1076+
* @memberof rules
1077+
* @readonly
1078+
* @type {Function}
1079+
* @see {@link module:@stdlib/_tools/eslint/rules/tsdoc-declarations-doctest}
1080+
*/
1081+
setReadOnly( rules, 'tsdoc-declarations-doctest', require( '@stdlib/_tools/eslint/rules/tsdoc-declarations-doctest' ) );
1082+
10741083
/**
10751084
* @name uppercase-required-constants
10761085
* @memberof rules
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
<!--
2+
3+
@license Apache-2.0
4+
5+
Copyright (c) 2025 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+
# tsdoc-declarations-doctest
22+
23+
> [ESLint rule][eslint-rules] to ensure that return annotations in TSDoc examples match the actual output in TypeScript declaration files (`*.d.ts`).
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/tsdoc-declarations-doctest' );
37+
```
38+
39+
#### rule
40+
41+
[ESLint rule][eslint-rules] to ensure that return annotations in TSDoc examples match the actual output in TypeScript declaration files (`*.d.ts`).
42+
43+
**Bad**:
44+
45+
<!-- eslint-disable stdlib/tsdoc-declarations-doctest -->
46+
47+
```typescript
48+
/**
49+
* Adds two numbers.
50+
*
51+
* @param x - first number
52+
* @param y - second number
53+
* @returns sum of x and y
54+
*
55+
* @example
56+
* var result = add( 2, 3 );
57+
* // returns 6
58+
*/
59+
declare function add( x: number, y: number ): number;
60+
```
61+
62+
**Good**:
63+
64+
```typescript
65+
/**
66+
* Adds two numbers.
67+
*
68+
* @param x - first number
69+
* @param y - second number
70+
* @returns sum of x and y
71+
*
72+
* @example
73+
* var result = add( 2, 3 );
74+
* // returns 5
75+
*/
76+
declare function add( x: number, y: number ): number;
77+
```
78+
79+
</section>
80+
81+
<!-- /.usage -->
82+
83+
<section class="notes">
84+
85+
## Notes
86+
87+
- Return annotations may start with `returns`, `throws`, or `=>`. `returns` follow variable declarations or assignment expressions, whereas `=>` follow expression-only forms including `console.log` calls.
88+
- The rule validates `@example` blocks in TSDoc comments within `*.d.ts` files by resolving the corresponding implementation via the nearest `package.json` file in the same or a parent directory and using its `main` field.
89+
- The rule skips validation if the `package.json` file cannot be found or if the resolved implementation cannot be loaded.
90+
- Examples are executed in a sandboxed VM context with limited globals for security.
91+
- This rule is specifically designed for TypeScript declaration files and will only process files with a `*.d.ts` filename extension.
92+
93+
</section>
94+
95+
<!-- /.notes -->
96+
97+
<section class="examples">
98+
99+
## Examples
100+
101+
<!-- eslint no-undef: "error" -->
102+
103+
```javascript
104+
var Linter = require( 'eslint' ).Linter;
105+
var parser = require( '@typescript-eslint/parser' );
106+
var rule = require( '@stdlib/_tools/eslint/rules/tsdoc-declarations-doctest' );
107+
108+
var linter = new Linter();
109+
110+
// Register the TypeScript parser and ESLint rule:
111+
linter.defineParser( '@typescript-eslint/parser', parser );
112+
linter.defineRule( 'tsdoc-declarations-doctest', rule );
113+
114+
// Generate our source code with incorrect return annotation:
115+
var code = [
116+
'/**',
117+
'* Returns the absolute value of a number.',
118+
'*',
119+
'* @param x - input value',
120+
'* @returns absolute value',
121+
'*',
122+
'* @example',
123+
'* var result = abs( -3 );',
124+
'* // returns 2',
125+
'*/',
126+
'declare function abs( x: number ): number;',
127+
'',
128+
'export = abs;'
129+
].join( '\n' );
130+
131+
// Lint the code:
132+
var result = linter.verify( code, {
133+
'parser': '@typescript-eslint/parser',
134+
'parserOptions': {
135+
'ecmaVersion': 2018,
136+
'sourceType': 'module'
137+
},
138+
'rules': {
139+
'tsdoc-declarations-doctest': 'error'
140+
}
141+
}, {
142+
'filename': '/path/to/project/lib/node_modules/@stdlib/math/base/special/abs/docs/types/index.d.ts'
143+
});
144+
/* returns
145+
[
146+
{
147+
'ruleId': 'tsdoc-declarations-doctest',
148+
'severity': 2,
149+
'message': 'Displayed return value is `2`, but expected `3` instead',
150+
'line': 9,
151+
'column': 1,
152+
'nodeType': null,
153+
'endLine': 10,
154+
'endColumn': 37
155+
}
156+
]
157+
*/
158+
```
159+
160+
</section>
161+
162+
<!-- /.examples -->
163+
164+
<!-- Section for related `stdlib` packages. Do not manually edit this section, as it is automatically populated. -->
165+
166+
<section class="related">
167+
168+
</section>
169+
170+
<!-- /.related -->
171+
172+
<!-- Section for all links. Make sure to keep an empty line after the `section` element and another before the `/section` close. -->
173+
174+
<section class="links">
175+
176+
[eslint-rules]: https://eslint.org/docs/developer-guide/working-with-rules
177+
178+
</section>
179+
180+
<!-- /.links -->
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* @license Apache-2.0
3+
*
4+
* Copyright (c) 2025 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 parser = require( '@typescript-eslint/parser' );
23+
var rule = require( './../lib' );
24+
25+
var linter = new Linter();
26+
27+
// Register the TypeScript parser and ESLint rule:
28+
linter.defineParser( '@typescript-eslint/parser', parser );
29+
linter.defineRule( 'tsdoc-declarations-doctest', rule );
30+
31+
// Generate our source code with incorrect return annotation:
32+
var code = [
33+
'/**',
34+
'* Returns the absolute value of a number.',
35+
'*',
36+
'* @param x - input value',
37+
'* @returns absolute value',
38+
'*',
39+
'* @example',
40+
'* var result = abs( -3 );',
41+
'* // returns 2',
42+
'*/',
43+
'declare function abs( x: number ): number;',
44+
'',
45+
'export = abs;'
46+
].join( '\n' );
47+
48+
// Lint the code:
49+
var result = linter.verify( code, {
50+
'parser': '@typescript-eslint/parser',
51+
'parserOptions': {
52+
'ecmaVersion': 2018,
53+
'sourceType': 'module'
54+
},
55+
'rules': {
56+
'tsdoc-declarations-doctest': 'error'
57+
}
58+
}, {
59+
'filename': 'lib/node_modules/@stdlib/math/base/special/abs/docs/types/index.d.ts'
60+
});
61+
62+
console.log( result );
63+
/* =>
64+
[
65+
{
66+
'ruleId': 'tsdoc-declarations-doctest',
67+
'severity': 2,
68+
'message': 'Displayed return value is `2`, but expected `3` instead',
69+
'line': 9,
70+
'column': 1,
71+
'nodeType': null,
72+
'endLine': 10,
73+
'endColumn': 37
74+
}
75+
]
76+
*/
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* @license Apache-2.0
3+
*
4+
* Copyright (c) 2025 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+
// VARIABLES //
22+
23+
// Regular expression to match function declarations such as "declare function abs( x: number ): number;" (captures function name):
24+
var RE_DECLARE_FUNCTION = /declare\s+function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[<(]/;
25+
26+
// Regular expression to match variable declarations such as "declare var someVar: SomeType;" (captures variable name):
27+
var RE_DECLARE_VAR = /declare\s+var\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/;
28+
29+
// Regular expression to match class declarations such as "declare class Complex64Array {" (captures class name):
30+
var RE_DECLARE_CLASS = /declare\s+class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s/;
31+
32+
// Regular expression to match const declarations such as "declare const PI: number;" (captures constant name):
33+
var RE_DECLARE_CONST = /declare\s+const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/;
34+
35+
// Regular expression to match variable declarations with interface types such as "declare var ctor: Int32Vector;" (captures variable name and interface name):
36+
var RE_DECLARE_VAR_INTERFACE = /declare\s+var\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:\s*([A-Z][a-zA-Z0-9_$]*)/;
37+
38+
39+
// MAIN //
40+
41+
/**
42+
* Adds a package export to the scope based on TypeScript declarations.
43+
*
44+
* @private
45+
* @param {Object} scope - VM scope object to add the package export to
46+
* @param {*} pkg - package export value to be added to scope
47+
* @param {string} sourceText - TypeScript declaration source text to parse for identifier names
48+
*/
49+
function addPackageToScope( scope, pkg, sourceText ) {
50+
var interfaceMatch;
51+
var namespaceMatch;
52+
var pkgType;
53+
var match;
54+
55+
pkgType = typeof pkg;
56+
if ( pkgType === 'function' ) {
57+
match = sourceText.match( RE_DECLARE_FUNCTION ) || sourceText.match( RE_DECLARE_VAR ) || sourceText.match( RE_DECLARE_CLASS ); // eslint-disable-line max-len
58+
if ( match ) {
59+
scope[ match[1] ] = pkg;
60+
}
61+
interfaceMatch = sourceText.match( RE_DECLARE_VAR_INTERFACE );
62+
if ( interfaceMatch ) {
63+
// Make the function available under both the variable and interface names:
64+
scope[ interfaceMatch[1] ] = pkg; // e.g., ctor
65+
scope[ interfaceMatch[2] ] = pkg; // e.g., Int32Vector
66+
}
67+
} else {
68+
// For objects, check if declared with interface pattern (e.g., `declare var ns: Namespace;`)...
69+
if ( pkgType === 'object' && pkg !== null ) {
70+
namespaceMatch = sourceText.match( RE_DECLARE_VAR_INTERFACE );
71+
if ( namespaceMatch ) {
72+
scope[ namespaceMatch[1] ] = pkg;
73+
}
74+
// Note: intentional fall-through to handle multiple patterns for objects...
75+
}
76+
// For all non-function types (including objects), also check const pattern (e.g., `declare const PI: number;` or `declare const obj: {...};`)...
77+
match = sourceText.match( RE_DECLARE_CONST );
78+
if ( match ) {
79+
scope[ match[1] ] = pkg;
80+
}
81+
}
82+
}
83+
84+
85+
// EXPORTS //
86+
87+
module.exports = addPackageToScope;

0 commit comments

Comments
 (0)