diff --git a/etc/eslint/plugins/typescript.js b/etc/eslint/plugins/typescript.js index 5553eb157e03..5775526ae0a2 100644 --- a/etc/eslint/plugins/typescript.js +++ b/etc/eslint/plugins/typescript.js @@ -30,7 +30,10 @@ var plugins = [ 'eslint-plugin-jsdoc', // Required for TypeScript support: - '@typescript-eslint' + '@typescript-eslint', + + // Stdlib custom rules: + 'stdlib' ]; diff --git a/etc/eslint/rules/typescript.js b/etc/eslint/rules/typescript.js index 1639beb44f67..f4c7a6cdb624 100644 --- a/etc/eslint/rules/typescript.js +++ b/etc/eslint/rules/typescript.js @@ -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 // diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/lib/index.js b/lib/node_modules/@stdlib/_tools/eslint/rules/lib/index.js index e7a2c4f7ce7d..ffd13415e68f 100644 --- a/lib/node_modules/@stdlib/_tools/eslint/rules/lib/index.js +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/lib/index.js @@ -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 diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/README.md b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/README.md new file mode 100644 index 000000000000..7a8f286db962 --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/README.md @@ -0,0 +1,194 @@ + + +# tsdoc-doctest + +> [ESLint rule][eslint-rules] to ensure that return annotations in TSDoc examples match the actual output. + +
+ +
+ + + +
+ +## 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**: + + + +```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; +``` + +
+ + + +
+ +## Examples + + + +```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 + } + ] +*/ +``` + +
+ + + +
+ +## Notes + +- The rule requires that the TypeScript declaration file path follows the stdlib convention: `lib/node_modules/@stdlib//docs/types/index.d.ts` +- The corresponding JavaScript package must be loadable via `require('@stdlib/')` +- The rule skips validation if the package cannot be loaded +- Examples are executed in a sandboxed VM context with limited globals + +
+ + + + + + + + + + + + + + diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/examples/index.js b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/examples/index.js new file mode 100644 index 000000000000..421c7bc433ec --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/examples/index.js @@ -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 + } + ] +*/ diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/lib/add_package_to_scope.js b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/lib/add_package_to_scope.js new file mode 100644 index 000000000000..54771dd152a3 --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/lib/add_package_to_scope.js @@ -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; diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/lib/create_vm_scope.js b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/lib/create_vm_scope.js new file mode 100644 index 000000000000..004c3c94e57e --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/lib/create_vm_scope.js @@ -0,0 +1,83 @@ +/** +* @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'; + +// MODULES // + +var proc = require( 'process' ); +var format = require( 'util' ).format; +var Buffer = require( '@stdlib/buffer/ctor' ); +var isString = require( '@stdlib/assert/is-string' ).isPrimitive; +var windowShim = require( './window.js' ); + + +// FUNCTIONS // + +/** +* Shim for `console.log` function that returns the logged data instead of printing it to `stdout`. +* +* @private +* @param {*} data - data to be logged +* @param {...*} args - substitution values +* @returns {*} return value +*/ +function log( data ) { + if ( isString( data ) ) { + return format.apply( null, arguments ); + } + return data; +} + + +// MAIN // + +/** +* Creates a VM execution scope with necessary globals. +* +* @param {string} dir - directory path +* @param {string} filename - file name +* @returns {Object} VM scope object +*/ +function createVMScope( dir, filename ) { + return { + 'require': require, + 'exports': exports, // eslint-disable-line node/exports-style + 'module': module, + 'process': proc, + 'setTimeout': setTimeout, + 'clearTimeout': clearTimeout, + 'setInterval': setInterval, + 'clearInterval': clearInterval, + 'window': windowShim, + 'Buffer': Buffer, + '__dirname': dir, + '__filename': filename, + 'console': { + 'dir': log, + 'error': log, + 'log': log, + 'warn': log + } + }; +} + + +// EXPORTS // + +module.exports = createVMScope; diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/lib/index.js b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/lib/index.js new file mode 100644 index 000000000000..bb012d0f7c87 --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/lib/index.js @@ -0,0 +1,39 @@ +/** +* @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'; + +/** +* ESLint rule to ensure return annotations in TypeScript declaration examples match the actual output. +* +* @module @stdlib/_tools/eslint/rules/tsdoc-doctest +* +* @example +* var rule = require( '@stdlib/_tools/eslint/rules/tsdoc-doctest' ); +* +* console.log( rule ); +*/ + +// MODULES // + +var main = require( './main.js' ); + + +// EXPORTS // + +module.exports = main; diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/lib/main.js b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/lib/main.js new file mode 100644 index 000000000000..29547bfaf69c --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/lib/main.js @@ -0,0 +1,446 @@ +/** +* @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'; + +// MODULES // + +var vm = require( 'vm' ); +var dirname = require( 'path' ).dirname; +var resolve = require( 'path' ).resolve; +var existsSync = require( 'fs' ).existsSync; // eslint-disable-line node/no-sync +var logger = require( 'debug' ); +var isNull = require( '@stdlib/assert/is-null' ); +var isNumber = require( '@stdlib/assert/is-number' ); +var contains = require( '@stdlib/assert/contains' ); +var replace = require( '@stdlib/string/replace' ); +var objectKeys = require( '@stdlib/utils/keys' ); +var compareValues = require( '@stdlib/_tools/doctest/compare-values' ); +var createAnnotationValue = require( '@stdlib/_tools/doctest/create-annotation-value' ); +var createVMScope = require( './create_vm_scope.js' ); +var addPackageToScope = require( './add_package_to_scope.js' ); +var SCOPE_DEFAULTS = require( './scope_defaults.json' ); + + +// VARIABLES // + +var debug = logger( 'tsdoc-doctest' ); +var RE_TSDOC = /\/\*\*[\s\S]+?\*\//g; +var RE_EXAMPLE = /@example\s*([\s\S]*?)(?=\n\s*@\w|\*\/|$)/g; +var RE_NEWLINE = /\r?\n/g; +var RE_ANNOTATION = /(?:\n|^)(?:var|let|const)? ?([a-zA-Z0-9._]+) ?=[^;]*?;\n\/\/ ?(returns|([A-Za-z][A-Za-z_0-9]*)? ?=>|throws) {0,1}([\s\S]*?)(\n|$)/g; +var RE_CONSOLE = /console\.(?:dir|error|log)/; +var RE_COMMENT_PREFIX = /^\s*\*\s?/gm; +var rule; + + +// FUNCTIONS // + +/** +* Counts the number of lines in the given string. +* +* @private +* @param {string} str - input string +* @returns {number} number of lines +*/ +function countLines( str ) { + return ( str.match( RE_NEWLINE ) || '' ).length; +} + +/** +* Searches for variable in scope matching the expected value. +* +* @private +* @param {Object} scope - VM scope +* @param {any} expected - expected value to search for +* @returns {string} variable name or `?` if no match found +*/ +function findName( scope, expected ) { + var keys = objectKeys( scope ); + var key; + var i; + for ( i = 0; i < keys.length; i++ ) { + key = keys[ i ]; + if ( + !contains( SCOPE_DEFAULTS, key ) && + !compareValues( scope[ key ], expected ) + ) { + return key; + } + } + return '?'; +} + +/** +* Resolves the implementation path relative to a TypeScript declaration file. +* +* @private +* @param {string} filepath - TypeScript declaration file path +* @param {string} implementationPath - relative path to implementation +* @returns {string|null} resolved implementation path or null if not found +*/ +function resolveImplementationPath( filepath, implementationPath ) { + var implPath; + var baseDir; + + baseDir = dirname( resolve( filepath ) ); + implPath = resolve( baseDir, implementationPath ); + if ( existsSync( implPath ) ) { + return implPath; + } + return null; +} + +/** +* Cleans TSDoc comment by removing comment prefixes. +* +* @private +* @param {string} comment - TSDoc comment +* @returns {string} cleaned comment +*/ +function cleanTSDocComment( comment ) { + // Remove opening /** and closing */ + var cleaned = comment.replace( /^\/\*\*/, '' ).replace( /\*\/$/, '' ); + + // Remove * at the beginning of lines + cleaned = replace( cleaned, RE_COMMENT_PREFIX, '' ); + return cleaned; +} + +/** +* Processes a single example code block, finding and validating annotations. +* +* @private +* @param {string} code - example code +* @param {number} commentIdx - comment index +* @param {Array} comments - all comments +* @param {Object} scope - VM execution scope +* @param {Function} report - error reporting function +* @param {Object} opts - options object +* @param {Object} sourceCode - source code object +*/ +function processExampleCode( code, commentIdx, comments, scope, report, opts, sourceCode ) { + var returnAnnotationPattern; + var valueStartInAnnotation; + var commentStartIdx; + var annotationMatch; + var annotationStart; + var exampleStartIdx; + var annotationEnd; + var intermediary; + var replacement; + var exampleIdx; + var remaining; + var expected; + var codeIdx; + var actual; + var last; + var line; + var type; + var loc; + var msg; + var out; + var arr; + + // VM context already created in validate() function + + last = 0; + RE_ANNOTATION.lastIndex = 0; + try { + arr = RE_ANNOTATION.exec( code ); + while ( !isNull( arr ) ) { + // Run intermediary code + intermediary = code.substring( last, arr.index ); + last = arr.index + arr[ 0 ].length; + if ( intermediary ) { + vm.runInContext( intermediary, scope ); + } + + // Calculate line of current code chunk within the comment + commentStartIdx = sourceCode.text.indexOf( comments[ commentIdx ] ); + + // Find the annotation in the original comment + returnAnnotationPattern = new RegExp( '// ' + arr[ 2 ] + '\\s+' + arr[ 4 ].replace( /[.*+?^${}()|[\]\\]/g, '\\$&' ) ); + annotationMatch = comments[ commentIdx ].match( returnAnnotationPattern ); + + if ( !annotationMatch ) { + continue; + } + + codeIdx = comments[ commentIdx ].indexOf( annotationMatch[ 0 ] ); + line = countLines( sourceCode.text.substring( 0, commentStartIdx + codeIdx ) ) + 1; + loc = { + 'start': { + 'line': line, + 'column': 0 + }, + 'end': { + 'line': line + countLines( arr[ 0 ] ), + 'column': arr[ 0 ].length + } + }; + + // Run code preceding return annotation + try { + out = vm.runInContext( arr[ 0 ], scope ); + if ( RE_CONSOLE.test( arr[ 1 ] ) ) { + actual = out; + } else { + actual = scope[ arr[ 1 ] ]; + } + if ( arr[ 3 ] ) { + actual = vm.runInContext( arr[ 3 ], scope ); + } + expected = arr[ 4 ]; + msg = compareValues( actual, expected ); + if ( msg ) { + opts.includeDecimal = isNumber( actual ) && contains( expected, '.' ); + replacement = createAnnotationValue( actual, opts ); + + // Find the position of the return value in the annotation + valueStartInAnnotation = annotationMatch[ 0 ].indexOf( arr[ 4 ] ); + loc.range = [ + // Position of arr[4] start in source text + commentStartIdx + codeIdx + valueStartInAnnotation, + + // Position of arr[4] end in source text + commentStartIdx + codeIdx + valueStartInAnnotation + arr[ 4 ].length + ]; + report( loc, msg, replacement ); + } + } catch ( err ) { + type = '<'+err.name+'>'; + if ( arr[ 2 ] !== 'throws' ) { + if ( arr[ 3 ] ) { + msg = 'Encountered an error: `'+err.message+'`.'; + } else { + msg = 'Encountered an error while running code: `'+err.message+'`. '; + msg += 'Did you mean to include a `// throws '+type+'` annotation instead of `// '+arr[ 2 ]+' '+arr[ 4 ]+'`?'; + } + } + else if ( arr[ 4 ] !== type ) { + msg = 'Code should have a `// throws '+type+'` annotation, but received: `// '+arr[ 2 ]+' '+arr[ 4 ]+'`'; + } + if ( msg ) { + replacement = ( arr[ 3 ] ) ? findName( scope, arr[ 4 ] ) + ' => ' + arr[ 4 ] : 'throws '+type; + + // Find annotation part in the match (after "// ") + annotationStart = arr[ 0 ].indexOf( '// ' ) + 3; + annotationEnd = arr[ 0 ].length - arr[ 5 ].length; + loc.range = [ + commentStartIdx + codeIdx + annotationStart, + commentStartIdx + codeIdx + annotationEnd + ]; + report( loc, msg, replacement ); + } + } + arr = RE_ANNOTATION.exec( code ); + } + + // Run any remaining code + remaining = code.substring( last ); + if ( remaining ) { + vm.runInContext( remaining, scope ); + } + } catch ( err ) { + // Calculate the line number for the example that caused the error + exampleStartIdx = sourceCode.text.indexOf( comments[ commentIdx ] ); + exampleIdx = comments[ commentIdx ].indexOf( code ); + line = countLines( sourceCode.text.substring( 0, exampleStartIdx + exampleIdx ) ) + 1; + loc = { + 'start': { + 'line': line, + 'column': 0 + }, + 'end': { + 'line': line + countLines( code ), + 'column': 0 + } + }; + + // Do not report errors in TypeScript declaration files due to modules failing to load + if ( contains( err.message, 'Cannot find module' ) ) { + return; + } + report( loc, 'Encountered an error while running example code: '+err.message ); + } +} + + +// MAIN // + +/** +* Rule for validating that return annotations in TypeScript declaration examples match the actual output. +* +* @param {Object} context - ESLint context +* @returns {Object} validators +*/ +function main( context ) { + var sourceCode; + var filename; + var options; + var dir; + + sourceCode = context.getSourceCode(); + filename = context.getFilename(); + options = context.options[ 0 ] || {}; + + // Only process TypeScript declaration files + if ( !filename.endsWith( '.d.ts' ) ) { + return {}; + } + + dir = dirname( filename ); + + /** + * Reports the error message. + * + * @private + * @param {Object} loc - error location info + * @param {string} msg - error message + * @param {string} replacement - fixed return annotation + */ + function report( loc, msg, replacement ) { + var result = { + 'message': msg, + 'loc': loc + }; + if ( replacement ) { + result.fix = fix; + } + context.report( result ); + + /** + * Applies a fix to the offending code. + * + * @private + * @param {Object} fixer - object to apply a fix + * @returns {Object} fixing object + */ + function fix( fixer ) { + return fixer.replaceTextRange( loc.range, replacement ); + } + } + + /** + * Validates examples in TSDoc comments. + * + * @private + */ + function validate() { + var implementationPath; + var implPath; + var examples; + var comments; + var example; + var cleaned; + var comment; + var scope; + var code; + var opts; + var pkg; + var i; + var j; + + // Resolve implementation path relative to TypeScript declaration file path: + implementationPath = options.implementationPath || '../../lib'; + implPath = resolveImplementationPath( filename, implementationPath ); + if ( !implPath ) { + debug( 'Could not resolve implementation path: ' + implementationPath + ' from ' + filename ); + return; + } + + // Try to load the implementation directly: + try { + pkg = require( implPath ); // eslint-disable-line stdlib/no-dynamic-require + } catch ( err ) { + debug( 'Could not load implementation: ' + implPath + '. Error: ' + err.message ); + return; + } + + // Get all TSDoc comments: + comments = sourceCode.text.match( RE_TSDOC ); + if ( !comments ) { + return; + } + + // Create a single VM scope and context for all examples: + scope = createVMScope( dir, filename ); + addPackageToScope( scope, pkg, sourceCode.text ); + vm.createContext( scope ); + + opts = { + 'includeDecimal': false + }; + + for ( i = 0; i < comments.length; i++ ) { + comment = comments[ i ]; + cleaned = cleanTSDocComment( comment ); + + // Extract @example blocks: + RE_EXAMPLE.lastIndex = 0; + examples = []; + example = RE_EXAMPLE.exec( cleaned ); + while ( example !== null ) { + examples.push( example[ 1 ] ); + example = RE_EXAMPLE.exec( cleaned ); + } + + for ( j = 0; j < examples.length; j++ ) { + code = examples[ j ].trim(); + + // Process the example code and validate annotations (reusing VM context): + processExampleCode( code, i, comments, scope, report, opts, sourceCode ); + } + } + } + + return { + 'Program': validate + }; +} + + +// MAIN // + +rule = { + 'meta': { + 'docs': { + 'description': 'ensure return annotations in TSDoc examples match the actual output' + }, + 'fixable': 'code', + 'schema': [ + { + 'type': 'object', + 'properties': { + 'implementationPath': { + 'type': 'string', + 'default': '../../lib' + } + }, + 'additionalProperties': false + } + ] + }, + 'create': main +}; + + +// EXPORTS // + +module.exports = rule; diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/lib/scope_defaults.json b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/lib/scope_defaults.json new file mode 100644 index 000000000000..fab6c5ea05dc --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/lib/scope_defaults.json @@ -0,0 +1,15 @@ +[ + "require", + "exports", + "module", + "process", + "setTimeout", + "clearTimeout", + "setInterval", + "clearInterval", + "window", + "Buffer", + "__dirname", + "__filename", + "console" +] diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/lib/window.js b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/lib/window.js new file mode 100644 index 000000000000..ce9936203e42 --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/lib/window.js @@ -0,0 +1,35 @@ +/** +* @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'; + +// MODULES // + +var noop = require( '@stdlib/utils/noop' ); + + +// MAIN // + +var shim = { + 'open': noop +}; + + +// EXPORTS // + +module.exports = shim; diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/package.json b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/package.json new file mode 100644 index 000000000000..a76dd13fbcd8 --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/package.json @@ -0,0 +1,54 @@ +{ + "name": "@stdlib/_tools/eslint/rules/tsdoc-doctest", + "version": "0.0.0", + "description": "ESLint rule to ensure return annotations in TSDoc examples match the actual output.", + "license": "Apache-2.0", + "author": { + "name": "The Stdlib Authors", + "url": "https://github.com/stdlib-js/stdlib/graphs/contributors" + }, + "contributors": [ + { + "name": "The Stdlib Authors", + "url": "https://github.com/stdlib-js/stdlib/graphs/contributors" + } + ], + "main": "./lib", + "directories": { + "example": "./examples", + "lib": "./lib", + "test": "./test" + }, + "scripts": {}, + "homepage": "https://github.com/stdlib-js/stdlib", + "repository": { + "type": "git", + "url": "git://github.com/stdlib-js/stdlib.git" + }, + "bugs": { + "url": "https://github.com/stdlib-js/stdlib/issues" + }, + "dependencies": {}, + "devDependencies": {}, + "engines": { + "node": ">=0.10.0", + "npm": ">2.7.0" + }, + "keywords": [ + "stdlib", + "tools", + "tool", + "eslint", + "lint", + "custom", + "rule", + "doctest", + "tsdoc", + "typescript" + ], + "__stdlib__": { + "envs": { + "node": true + } + } +} diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/test/fixtures/invalid.js b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/test/fixtures/invalid.js new file mode 100644 index 000000000000..6a74a3955af7 --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/test/fixtures/invalid.js @@ -0,0 +1,164 @@ +/** +* @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'; + +// MODULES // + +var join = require( 'path' ).join; +var rootDir = require( '@stdlib/_tools/utils/root-dir' ); + + +// VARIABLES // + +var ROOT_DIR = rootDir(); +var invalid = []; +var test; + + +// TESTS // + +test = { + 'code': [ + '/**', + '* Returns the absolute value.', + '*', + '* @param x - input value', + '* @returns absolute value', + '*', + '* @example', + '* var v = abs( -1.0 );', + '* // returns 2.0', + '*/', + 'declare function abs( x: number ): number;', + '', + 'export = abs;' + ].join( '\n' ), + 'filename': join( ROOT_DIR, 'lib/node_modules/@stdlib/math/base/special/abs/docs/types/index.d.ts' ), + 'output': [ + '/**', + '* Returns the absolute value.', + '*', + '* @param x - input value', + '* @returns absolute value', + '*', + '* @example', + '* var v = abs( -1.0 );', + '* // returns 1', + '*/', + 'declare function abs( x: number ): number;', + '', + 'export = abs;' + ].join( '\n' ), + 'errors': [ + { + 'message': 'Displayed return value is `2.0`, but expected `1` instead', + 'type': null + } + ] +}; +invalid.push( test ); + +test = { + 'code': [ + '/**', + '* Returns the square root.', + '*', + '* @param x - input value', + '* @returns square root', + '*', + '* @example', + '* var v = sqrt( 4.0 );', + '* // returns 3.0', + '*', + '* @example', + '* var v = sqrt( 9.0 );', + '* // returns 3.0', + '*/', + 'declare function sqrt( x: number ): number;', + '', + 'export = sqrt;' + ].join( '\n' ), + 'filename': join( ROOT_DIR, 'lib/node_modules/@stdlib/math/base/special/sqrt/docs/types/index.d.ts' ), + 'output': [ + '/**', + '* Returns the square root.', + '*', + '* @param x - input value', + '* @returns square root', + '*', + '* @example', + '* var v = sqrt( 4.0 );', + '* // returns 2', + '*', + '* @example', + '* var v = sqrt( 9.0 );', + '* // returns 3.0', + '*/', + 'declare function sqrt( x: number ): number;', + '', + 'export = sqrt;' + ].join( '\n' ), + 'errors': [ + { + 'message': 'Displayed return value is `3.0`, but expected `2` instead', + 'type': null + } + ] +}; +invalid.push( test ); + +test = { + 'code': [ + '/**', + '* The mathematical constant pi.', + '*', + '* @example', + '* var pi = PI;', + '* // returns 3.14', + '*/', + 'declare const PI: number;', + '', + 'export = PI;' + ].join( '\n' ), + 'filename': join( ROOT_DIR, 'lib/node_modules/@stdlib/constants/float64/pi/docs/types/index.d.ts' ), + 'output': [ + '/**', + '* The mathematical constant pi.', + '*', + '* @example', + '* var pi = PI;', + '* // returns ~3.142', + '*/', + 'declare const PI: number;', + '', + 'export = PI;' + ].join( '\n' ), + 'errors': [ + { + 'message': 'Displayed return value is `3.14`, but expected `3.141592653589793` instead', + 'type': null + } + ] +}; +invalid.push( test ); + + +// EXPORTS // + +module.exports = invalid; diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/test/fixtures/unvalidated.js b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/test/fixtures/unvalidated.js new file mode 100644 index 000000000000..413b6eae7042 --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/test/fixtures/unvalidated.js @@ -0,0 +1,69 @@ +/** +* @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'; + +// MODULES // + +var join = require( 'path' ).join; +var rootDir = require( '@stdlib/_tools/utils/root-dir' ); + + +// VARIABLES // + +var ROOT_DIR = rootDir(); +var unvalidated = []; +var test; + +// Test files without examples or non-TypeScript files +test = { + 'code': [ + '/**', + '* Function without @example block.', + '*/', + 'declare function noExample( x: number ): number;', + '', + 'export = noExample;' + ].join( '\n' ), + 'filename': join( ROOT_DIR, 'lib/node_modules/@stdlib/test/no-example/docs/types/index.d.ts' ) +}; +unvalidated.push( test ); + +test = { + 'code': [ + '/**', + '* Not a TypeScript file.', + '*', + '* @example', + '* var result = test();', + '* // returns true', + '*/', + 'function test() {', + ' return true;', + '}', + '', + 'module.exports = test;' + ].join( '\n' ), + 'filename': 'test.js' +}; +unvalidated.push( test ); + + +// EXPORTS // + +module.exports = unvalidated; diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/test/fixtures/valid.js b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/test/fixtures/valid.js new file mode 100644 index 000000000000..6a8ce939318b --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/test/fixtures/valid.js @@ -0,0 +1,162 @@ +/** +* @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'; + +// MODULES // + +var join = require( 'path' ).join; +var rootDir = require( '@stdlib/_tools/utils/root-dir' ); + + +// VARIABLES // + +var ROOT_DIR = rootDir(); +var valid = []; +var test; + + +// TESTS // + +test = { + 'code': [ + '/**', + '* Returns the absolute value.', + '*', + '* @param x - input value', + '* @returns absolute value', + '*', + '* @example', + '* var v = abs( -1.0 );', + '* // returns 1', + '*/', + 'declare function abs( x: number ): number;', + '', + 'export = abs;' + ].join( '\n' ), + 'filename': join( ROOT_DIR, 'lib/node_modules/@stdlib/math/base/special/abs/docs/types/index.d.ts' ) +}; +valid.push( test ); + +test = { + 'code': [ + '/**', + '* Returns the square root.', + '*', + '* @param x - input value', + '* @returns square root', + '*', + '* @example', + '* var v = sqrt( 4.0 );', + '* // returns 2', + '*', + '* @example', + '* var v = sqrt( 9.0 );', + '* // returns 3', + '*/', + 'declare function sqrt( x: number ): number;', + '', + 'export = sqrt;' + ].join( '\n' ), + 'filename': join( ROOT_DIR, 'lib/node_modules/@stdlib/math/base/special/sqrt/docs/types/index.d.ts' ) +}; +valid.push( test ); + +test = { + 'code': [ + '/**', + '* The mathematical constant pi.', + '*', + '* @example', + '* var pi = PI;', + '* // returns ~3.142', + '*/', + 'declare const PI: number;', + '', + 'export = PI;' + ].join( '\n' ), + 'filename': join( ROOT_DIR, 'lib/node_modules/@stdlib/constants/float64/pi/docs/types/index.d.ts' ) +}; +valid.push( test ); + +test = { + 'code': [ + '/**', + '* Complex number array constructor.', + '*', + '* @example', + '* var arr = new Complex64Array();', + '* // returns ', + '*', + '* var len = arr.length;', + '* // returns 0', + '*/', + 'declare class Complex64Array {', + ' constructor();', + ' readonly length: number;', + '}', + '', + 'declare var Complex64Array: {', + ' new(): Complex64Array;', + '};', + '', + 'export = Complex64Array;' + ].join( '\n' ), + 'filename': join( ROOT_DIR, 'lib/node_modules/@stdlib/array/complex64/docs/types/index.d.ts' ) +}; +valid.push( test ); + +test = { + 'code': [ + '/**', + '* Console log example.', + '*', + '* @param msg - message to log', + '*', + '* @example', + '* console.log( format( "Hello %s", "World" ) );', + '* // => \'Hello World\'', + '*/', + 'declare function format( template: string, ...args: any[] ): string;', + '', + 'export = format;' + ].join( '\n' ), + 'filename': join( ROOT_DIR, 'lib/node_modules/@stdlib/string/format/docs/types/index.d.ts' ) +}; +valid.push( test ); + +test = { + 'code': [ + '/**', + '* TypeScript declaration without examples.', + '*', + '* @param x - input value', + '* @returns doubled value', + '*/', + 'declare function double( x: number ): number;', + '', + 'export = double;' + ].join( '\n' ), + 'filename': join( ROOT_DIR, 'lib/node_modules/@stdlib/math/base/special/double/docs/types/index.d.ts' ) +}; +valid.push( test ); + + +// EXPORTS // + +module.exports = valid; diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/test/test.js b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/test/test.js new file mode 100644 index 000000000000..4a7a28dac555 --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/tsdoc-doctest/test/test.js @@ -0,0 +1,104 @@ +/** +* @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'; + +// MODULES // + +var tape = require( 'tape' ); +var RuleTester = require( 'eslint' ).RuleTester; +var rule = require( './../lib' ); + + +// FIXTURES // + +var valid = require( './fixtures/valid.js' ); +var invalid = require( './fixtures/invalid.js' ); +var unvalidated = require( './fixtures/unvalidated.js' ); + + +// TESTS // + +tape( 'main export is an object', function test( t ) { + t.ok( true, __filename ); + t.strictEqual( typeof rule, 'object', 'main export is an object' ); + t.end(); +}); + +tape( 'the function positively validates code where all return annotations inside of example code match the actual output', function test( t ) { + var tester = new RuleTester({ + 'parser': require.resolve( '@typescript-eslint/parser' ), + 'parserOptions': { + 'ecmaVersion': 2018, + 'sourceType': 'module' + } + }); + + try { + tester.run( 'tsdoc-doctest', rule, { + 'valid': valid, + 'invalid': [] + }); + t.pass( 'passed without errors' ); + } catch ( err ) { + t.fail( 'encountered an error: ' + err.message ); + } + t.end(); +}); + +tape( 'the function negatively validates code where not all return annotations inside of example code match the actual output', function test( t ) { + var tester = new RuleTester({ + 'parser': require.resolve( '@typescript-eslint/parser' ), + 'parserOptions': { + 'ecmaVersion': 2018, + 'sourceType': 'module' + } + }); + + try { + tester.run( 'tsdoc-doctest', rule, { + 'valid': [], + 'invalid': invalid + }); + t.pass( 'passed without errors' ); + } catch ( err ) { + t.fail( 'encountered an error: ' + err.message ); + } + t.end(); +}); + +tape( 'the function does not validate comments without TSDoc examples', function test( t ) { + var tester = new RuleTester({ + 'parser': require.resolve( '@typescript-eslint/parser' ), + 'parserOptions': { + 'ecmaVersion': 2018, + 'sourceType': 'module' + } + }); + + try { + tester.run( 'tsdoc-doctest', rule, { + 'valid': unvalidated, + 'invalid': [] + }); + t.pass( 'passed without errors' ); + } catch ( err ) { + t.fail( 'encountered an error: ' + err.message ); + } + t.end(); +});