-
-
Couldn't load subscription status.
- Fork 922
build: add tsdoc-doctest ESLint rule
#8039
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
8a617b9
115b700
2b5611b
66ddee1
08e5f06
fab2c2e
2cee94f
86ac0f0
1d346e5
eb49b5f
f965f20
911504b
d86dcff
1a1fc84
adbc303
75bc34d
7a94b5a
3fe88ba
6b0f007
27837f7
be53ab0
cf7406d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| <!-- | ||
|
|
||
| @license Apache-2.0 | ||
|
|
||
| Copyright (c) 2025 The Stdlib Authors. | ||
|
|
||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| You may obtain a copy of the License at | ||
|
|
||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
|
|
||
| Unless required by applicable law or agreed to in writing, software | ||
| distributed under the License is distributed on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| See the License for the specific language governing permissions and | ||
| limitations under the License. | ||
|
|
||
| --> | ||
|
|
||
| # tsdoc-declarations-doctest | ||
|
|
||
| > [ESLint rule][eslint-rules] to ensure that return annotations in TSDoc examples match the actual output in TypeScript declaration files (`*.d.ts`). | ||
|
|
||
| <section class="intro"> | ||
|
|
||
| </section> | ||
|
|
||
| <!-- /.intro --> | ||
|
|
||
| <section class="usage"> | ||
|
|
||
| ## Usage | ||
|
|
||
| ```javascript | ||
| var rule = require( '@stdlib/_tools/eslint/rules/tsdoc-declarations-doctest' ); | ||
| ``` | ||
|
|
||
| #### rule | ||
|
|
||
| [ESLint rule][eslint-rules] to ensure that return annotations in TSDoc examples match the actual output in TypeScript declaration files (`*.d.ts`). | ||
|
|
||
| **Bad**: | ||
|
|
||
| <!-- eslint-disable stdlib/tsdoc-declarations-doctest --> | ||
|
|
||
| ```typescript | ||
| /** | ||
| * Adds two numbers. | ||
| * | ||
| * @param x - first number | ||
| * @param y - second number | ||
| * @returns sum of x and y | ||
| * | ||
| * @example | ||
| * var result = add( 2, 3 ); | ||
| * // returns 6 | ||
| */ | ||
| declare function add( x: number, y: number ): number; | ||
| ``` | ||
|
|
||
| **Good**: | ||
|
|
||
| ```typescript | ||
| /** | ||
| * Adds two numbers. | ||
| * | ||
| * @param x - first number | ||
| * @param y - second number | ||
| * @returns sum of x and y | ||
| * | ||
| * @example | ||
| * var result = add( 2, 3 ); | ||
| * // returns 5 | ||
| */ | ||
| declare function add( x: number, y: number ): number; | ||
| ``` | ||
|
|
||
| </section> | ||
|
|
||
| <!-- /.usage --> | ||
|
|
||
| <section class="notes"> | ||
|
|
||
| ## Notes | ||
|
|
||
| - Return annotations may start with `returns`, `throws`, or `=>`. `returns` follow variable declarations or assignment expressions, whereas `=>` follow expression-only forms including `console.log` calls. | ||
| - 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. | ||
| - The rule skips validation if the `package.json` file cannot be found or if the resolved implementation cannot be loaded. | ||
| - Examples are executed in a sandboxed VM context with limited globals for security. | ||
| - This rule is specifically designed for TypeScript declaration files and will only process files with a `*.d.ts` filename extension. | ||
|
|
||
| </section> | ||
|
|
||
| <!-- /.notes --> | ||
|
|
||
| <section class="examples"> | ||
|
|
||
| ## Examples | ||
|
|
||
| <!-- eslint no-undef: "error" --> | ||
|
|
||
| ```javascript | ||
| var Linter = require( 'eslint' ).Linter; | ||
| var parser = require( '@typescript-eslint/parser' ); | ||
| var rule = require( '@stdlib/_tools/eslint/rules/tsdoc-declarations-doctest' ); | ||
|
|
||
| var linter = new Linter(); | ||
|
|
||
| // Register the TypeScript parser and ESLint rule: | ||
| linter.defineParser( '@typescript-eslint/parser', parser ); | ||
| linter.defineRule( 'tsdoc-declarations-doctest', rule ); | ||
|
|
||
| // Generate our source code with incorrect return annotation: | ||
| var code = [ | ||
| '/**', | ||
| '* Returns the absolute value of a number.', | ||
| '*', | ||
| '* @param x - input value', | ||
| '* @returns absolute value', | ||
| '*', | ||
| '* @example', | ||
| '* var result = abs( -3 );', | ||
| '* // returns 2', | ||
| '*/', | ||
| 'declare function abs( x: number ): number;', | ||
| '', | ||
| 'export = abs;' | ||
| ].join( '\n' ); | ||
|
|
||
| // Lint the code: | ||
| var result = linter.verify( code, { | ||
| 'parser': '@typescript-eslint/parser', | ||
| 'parserOptions': { | ||
| 'ecmaVersion': 2018, | ||
| 'sourceType': 'module' | ||
| }, | ||
| 'rules': { | ||
| 'tsdoc-declarations-doctest': 'error' | ||
| } | ||
| }, { | ||
| 'filename': '/path/to/project/lib/node_modules/@stdlib/math/base/special/abs/docs/types/index.d.ts' | ||
| }); | ||
| /* returns | ||
| [ | ||
| { | ||
| 'ruleId': 'tsdoc-declarations-doctest', | ||
| 'severity': 2, | ||
| 'message': 'Displayed return value is `2`, but expected `3` instead', | ||
| 'line': 9, | ||
| 'column': 1, | ||
| 'nodeType': null, | ||
| 'endLine': 10, | ||
| 'endColumn': 37 | ||
| } | ||
| ] | ||
| */ | ||
| ``` | ||
|
|
||
| </section> | ||
|
|
||
| <!-- /.examples --> | ||
|
|
||
| <!-- Section for related `stdlib` packages. Do not manually edit this section, as it is automatically populated. --> | ||
|
|
||
| <section class="related"> | ||
|
|
||
| </section> | ||
|
|
||
| <!-- /.related --> | ||
|
|
||
| <!-- Section for all links. Make sure to keep an empty line after the `section` element and another before the `/section` close. --> | ||
|
|
||
| <section class="links"> | ||
|
|
||
| [eslint-rules]: https://eslint.org/docs/developer-guide/working-with-rules | ||
|
|
||
| </section> | ||
|
|
||
| <!-- /.links --> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| /** | ||
| * @license Apache-2.0 | ||
| * | ||
| * Copyright (c) 2025 The Stdlib Authors. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| 'use strict'; | ||
|
|
||
| var Linter = require( 'eslint' ).Linter; | ||
| var parser = require( '@typescript-eslint/parser' ); | ||
| var rule = require( './../lib' ); | ||
|
|
||
| var linter = new Linter(); | ||
|
|
||
| // Register the TypeScript parser and ESLint rule: | ||
| linter.defineParser( '@typescript-eslint/parser', parser ); | ||
| linter.defineRule( 'tsdoc-declarations-doctest', rule ); | ||
|
|
||
| // Generate our source code with incorrect return annotation: | ||
| var code = [ | ||
| '/**', | ||
| '* Returns the absolute value of a number.', | ||
| '*', | ||
| '* @param x - input value', | ||
| '* @returns absolute value', | ||
| '*', | ||
| '* @example', | ||
| '* var result = abs( -3 );', | ||
| '* // returns 2', | ||
| '*/', | ||
| 'declare function abs( x: number ): number;', | ||
| '', | ||
| 'export = abs;' | ||
| ].join( '\n' ); | ||
|
|
||
| // Lint the code: | ||
| var result = linter.verify( code, { | ||
| 'parser': '@typescript-eslint/parser', | ||
| 'parserOptions': { | ||
| 'ecmaVersion': 2018, | ||
| 'sourceType': 'module' | ||
| }, | ||
| 'rules': { | ||
| 'tsdoc-declarations-doctest': 'error' | ||
| } | ||
| }, { | ||
| 'filename': 'lib/node_modules/@stdlib/math/base/special/abs/docs/types/index.d.ts' | ||
| }); | ||
|
|
||
| console.log( result ); | ||
| /* => | ||
| [ | ||
| { | ||
| 'ruleId': 'tsdoc-declarations-doctest', | ||
| 'severity': 2, | ||
| 'message': 'Displayed return value is `2`, but expected `3` instead', | ||
| 'line': 9, | ||
| 'column': 1, | ||
| 'nodeType': null, | ||
| 'endLine': 10, | ||
| 'endColumn': 37 | ||
| } | ||
| ] | ||
| */ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| /** | ||
| * @license Apache-2.0 | ||
| * | ||
| * Copyright (c) 2025 The Stdlib Authors. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| 'use strict'; | ||
|
|
||
| // VARIABLES // | ||
|
|
||
| // Regular expression to match function declarations such as "declare function abs( x: number ): number;" (captures function name): | ||
| var RE_DECLARE_FUNCTION = /declare\s+function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[<(]/; | ||
|
|
||
| // Regular expression to match variable declarations such as "declare var someVar: SomeType;" (captures variable name): | ||
| var RE_DECLARE_VAR = /declare\s+var\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/; | ||
|
|
||
| // Regular expression to match class declarations such as "declare class Complex64Array {" (captures class name): | ||
| var RE_DECLARE_CLASS = /declare\s+class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s/; | ||
|
|
||
| // Regular expression to match const declarations such as "declare const PI: number;" (captures constant name): | ||
| var RE_DECLARE_CONST = /declare\s+const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/; | ||
|
|
||
| // Regular expression to match variable declarations with interface types such as "declare var ctor: Int32Vector;" (captures variable name and interface name): | ||
| var RE_DECLARE_VAR_INTERFACE = /declare\s+var\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:\s*([A-Z][a-zA-Z0-9_$]*)/; | ||
|
|
||
|
|
||
| // MAIN // | ||
|
|
||
| /** | ||
| * Adds a package export to the scope based on TypeScript declarations. | ||
| * | ||
| * @private | ||
| * @param {Object} scope - VM scope object to add the package export to | ||
| * @param {*} pkg - package export value to be added to scope | ||
| * @param {string} sourceText - TypeScript declaration source text to parse for identifier names | ||
| */ | ||
| function addPackageToScope( scope, pkg, sourceText ) { | ||
| var interfaceMatch; | ||
| var namespaceMatch; | ||
| var pkgType; | ||
| var match; | ||
|
|
||
| pkgType = typeof pkg; | ||
| if ( pkgType === 'function' ) { | ||
| match = sourceText.match( RE_DECLARE_FUNCTION ) || | ||
| sourceText.match( RE_DECLARE_VAR ) || | ||
| sourceText.match( RE_DECLARE_CLASS ); | ||
| if ( match ) { | ||
| scope[ match[1] ] = pkg; | ||
| } | ||
| interfaceMatch = sourceText.match( RE_DECLARE_VAR_INTERFACE ); | ||
| if ( interfaceMatch ) { | ||
| // Make the function available under both the variable and interface names: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a little bit odd, but I understand why this is being done this way. Namely, My sense is this could break at some point, where we double alias, which then clashes with a real alias. But we can cross that bridge when we get there. |
||
| scope[ interfaceMatch[1] ] = pkg; // e.g., ctor | ||
| scope[ interfaceMatch[2] ] = pkg; // e.g., Int32Vector | ||
| } | ||
| } else { | ||
| if ( pkgType === 'object' && pkg !== null ) { | ||
| namespaceMatch = sourceText.match( RE_DECLARE_VAR_INTERFACE ); | ||
| if ( namespaceMatch ) { | ||
| scope[ namespaceMatch[1] ] = pkg; | ||
Planeshifter marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| match = sourceText.match( RE_DECLARE_CONST ); | ||
| if ( match ) { | ||
| scope[ match[1] ] = pkg; | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| // EXPORTS // | ||
|
|
||
| module.exports = addPackageToScope; | ||
Uh oh!
There was an error while loading. Please reload this page.