Skip to content
Draft
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
5 changes: 4 additions & 1 deletion etc/eslint/plugins/typescript.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ var plugins = [
'eslint-plugin-jsdoc',

// Required for TypeScript support:
'@typescript-eslint'
'@typescript-eslint',

// Stdlib custom rules:
'stdlib'
];


Expand Down
11 changes: 11 additions & 0 deletions etc/eslint/rules/typescript.js
Original file line number Diff line number Diff line change
Expand Up @@ -2712,6 +2712,17 @@ rules[ 'yoda' ] = 'error';
*/
rules[ 'expect-type/expect' ] = 'error';

/**
* Ensures return annotations in TSDoc examples match the actual output.
*
* @name stdlib/tsdoc-doctest
* @memberof rules
* @type {string}
* @default 'error'
* @see {@link module:@stdlib/_tools/eslint/rules/tsdoc-doctest}
*/
rules[ 'stdlib/tsdoc-doctest' ] = 'error';


// EXPORTS //

Expand Down
9 changes: 9 additions & 0 deletions lib/node_modules/@stdlib/_tools/eslint/rules/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,15 @@ setReadOnly( rules, 'section-headers', require( '@stdlib/_tools/eslint/rules/sec
*/
setReadOnly( rules, 'ternary-condition-parentheses', require( '@stdlib/_tools/eslint/rules/ternary-condition-parentheses' ) );

/**
* @name tsdoc-doctest
* @memberof rules
* @readonly
* @type {Function}
* @see {@link module:@stdlib/_tools/eslint/rules/tsdoc-doctest}
*/
setReadOnly( rules, 'tsdoc-doctest', require( '@stdlib/_tools/eslint/rules/tsdoc-doctest' ) );

/**
* @name uppercase-required-constants
* @memberof rules
Expand Down
194 changes: 194 additions & 0 deletions lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
<!--

@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-doctest

> [ESLint rule][eslint-rules] to ensure that return annotations in TSDoc examples match the actual output.

<section class="intro">

</section>

<!-- /.intro -->

<section class="usage">

## Usage

```javascript
var rule = require( '@stdlib/_tools/eslint/rules/tsdoc-doctest' );
```

#### rule

[ESLint rule][eslint-rules] to ensure that return annotations in TSDoc examples match the actual output. The rule validates `@example` blocks in TSDoc comments within `.d.ts` files.

The rule:

- Extracts `@example` blocks from TSDoc comments in TypeScript declaration files
- Loads the actual JavaScript package corresponding to the declaration file
- Executes the example code in a sandboxed environment
- Compares the actual output with the expected return annotations
- Supports functions, constants, classes, and namespace objects
- Handles `// returns`, `// throws`, and `// =>` annotations

**Bad**:

<!-- eslint-disable stdlib/tsdoc-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;

export = add;
```

**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;

export = add;
```

</section>

<!-- /.usage -->

<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-doctest' );

var linter = new Linter();

// Register the TypeScript parser and ESLint rule:
linter.defineParser( '@typescript-eslint/parser', parser );
linter.defineRule( 'tsdoc-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-doctest': 'error'
}
}, {
'filename': 'lib/node_modules/@stdlib/math/base/special/abs/docs/types/index.d.ts'
});

console.log( result );
/* =>
[
{
'ruleId': 'tsdoc-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 class="notes">

## Notes

- The rule requires that the TypeScript declaration file path follows the stdlib convention: `lib/node_modules/@stdlib/<package>/docs/types/index.d.ts`
- The corresponding JavaScript package must be loadable via `require('@stdlib/<package>')`
- The rule skips validation if the package cannot be loaded
- Examples are executed in a sandboxed VM context with limited globals

</section>

<!-- /.notes -->

<!-- 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-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-doctest': 'error'
}
}, {
'filename': 'lib/node_modules/@stdlib/math/base/special/abs/docs/types/index.d.ts'
});

console.log( result );
/* =>
[
{
'ruleId': 'tsdoc-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,81 @@
/**
* @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 expressions for matching TypeScript declarations:
var RE_DECLARE_FUNCTION = /declare\s+function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[<(]/;
var RE_DECLARE_VAR = /declare\s+var\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/;
var RE_DECLARE_CLASS = /declare\s+class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s/;
var RE_DECLARE_VAR_NAMESPACE = /declare\s+var\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:\s*[A-Z][a-zA-Z0-9_$]*/;
var RE_DECLARE_CONST = /declare\s+const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/;


// MAIN //

/**
* Adds package export to scope based on TypeScript declarations.
*
* @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 namespaceMatch;
var funcMatch;

if ( typeof pkg === 'function' ) {
// Try to match declare function pattern (handles generics with <):
funcMatch = sourceText.match( RE_DECLARE_FUNCTION );
if ( !funcMatch ) {
// Try to match declare var pattern:
funcMatch = sourceText.match( RE_DECLARE_VAR );
}
if ( !funcMatch ) {
// Try to match declare class pattern (for constructor functions):
funcMatch = sourceText.match( RE_DECLARE_CLASS );
}
if ( funcMatch ) {
scope[ funcMatch[1] ] = pkg;
}
} else if ( typeof pkg === 'object' && pkg !== null ) {
// Handle namespace objects and other object interfaces:
namespaceMatch = sourceText.match( RE_DECLARE_VAR_NAMESPACE );
if ( namespaceMatch ) {
scope[ namespaceMatch[1] ] = pkg;
}
// Also check for const declarations (e.g., Complex64/Complex128 constants):
funcMatch = sourceText.match( RE_DECLARE_CONST );
if ( funcMatch ) {
scope[ funcMatch[1] ] = pkg;
}
} else {
// Try to match declare const pattern:
funcMatch = sourceText.match( RE_DECLARE_CONST );
if ( funcMatch ) {
scope[ funcMatch[1] ] = pkg;
}
}
}


// EXPORTS //

module.exports = addPackageToScope;
Loading
Loading