From 730cfb57d3829d3ad1256101592717b824b5f92f Mon Sep 17 00:00:00 2001 From: Philipp Burckhardt Date: Sat, 22 Aug 2026 00:40:44 -0500 Subject: [PATCH 1/4] feat: add `format-args` ESLint rule Adds a custom ESLint rule which verifies that calls to the format function exported by `@stdlib/string/format` are provided a number of arguments matching the number of placeholders in the format string. The rule resolves the callee symbol via scope analysis in order to guard against shadowing and unrelated symbols sharing the same name, and skips calls via `#.call` and `#.apply`, calls in which the format string is not a string literal, calls containing spread elements, and format strings containing positional placeholders. Co-Authored-By: Claude Fable 5 --- 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_pkg_readmes status: passed - task: lint_markdown_docs status: na - task: lint_markdown status: na - 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 --- --- etc/eslint/rules/stdlib.js | 24 ++- .../_tools/eslint/rules/format-args/README.md | 138 ++++++++++++ .../rules/format-args/examples/index.js | 52 +++++ .../eslint/rules/format-args/lib/index.js | 39 ++++ .../eslint/rules/format-args/lib/main.js | 203 ++++++++++++++++++ .../eslint/rules/format-args/package.json | 54 +++++ .../format-args/test/fixtures/invalid.js | 141 ++++++++++++ .../format-args/test/fixtures/unvalidated.js | 57 +++++ .../rules/format-args/test/fixtures/valid.js | 182 ++++++++++++++++ .../eslint/rules/format-args/test/test.js | 98 +++++++++ .../@stdlib/_tools/eslint/rules/lib/index.js | 9 + 11 files changed, 996 insertions(+), 1 deletion(-) create mode 100644 lib/node_modules/@stdlib/_tools/eslint/rules/format-args/README.md create mode 100644 lib/node_modules/@stdlib/_tools/eslint/rules/format-args/examples/index.js create mode 100644 lib/node_modules/@stdlib/_tools/eslint/rules/format-args/lib/index.js create mode 100644 lib/node_modules/@stdlib/_tools/eslint/rules/format-args/lib/main.js create mode 100644 lib/node_modules/@stdlib/_tools/eslint/rules/format-args/package.json create mode 100644 lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/invalid.js create mode 100644 lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/unvalidated.js create mode 100644 lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/valid.js create mode 100644 lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/test.js diff --git a/etc/eslint/rules/stdlib.js b/etc/eslint/rules/stdlib.js index 1c84b79de46c..deedb96f41f3 100644 --- a/etc/eslint/rules/stdlib.js +++ b/etc/eslint/rules/stdlib.js @@ -1,4 +1,4 @@ -/* eslint-disable stdlib/jsdoc-doctest-marker, stdlib/jsdoc-doctest, stdlib/jsdoc-example-require-spacing, stdlib/jsdoc-no-tabs */ +/* eslint-disable stdlib/jsdoc-doctest-marker, stdlib/jsdoc-example-require-spacing, stdlib/jsdoc-no-tabs */ /** * @license Apache-2.0 @@ -275,6 +275,28 @@ rules[ 'stdlib/empty-line-before-comment' ] = 'error'; */ rules[ 'stdlib/eol-open-bracket-spacing' ] = 'error'; +/** +* Require that format calls are provided an expected number of arguments. +* +* @name format-args +* @memberof rules +* @type {string} +* @default 'error' +* +* @example +* // Bad... +* var format = require( '@stdlib/string/format' ); +* +* var str = format( '%s %s', 'foo' ); +* +* @example +* // Good... +* var format = require( '@stdlib/string/format' ); +* +* var str = format( '%s %s', 'foo', 'bar' ); +*/ +rules[ 'stdlib/format-args' ] = 'error'; + /** * Require blockquotes to have `2` character indentation. * diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/README.md b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/README.md new file mode 100644 index 000000000000..41247f14d2aa --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/README.md @@ -0,0 +1,138 @@ + + +# format-args + +> [ESLint rule][eslint-rules] requiring that format calls are provided an expected number of arguments. + +
+ +
+ + + +
+ +## Usage + +```javascript +var rule = require( '@stdlib/_tools/eslint/rules/format-args' ); +``` + +#### rule + +[ESLint rule][eslint-rules] requiring that calls to the format function exported by [`@stdlib/string/format`][@stdlib/string/format] are provided a number of arguments matching the number of placeholders in the format string. + +**Bad**: + + + +```javascript +var format = require( '@stdlib/string/format' ); + +var str = format( '%s %s', 'foo' ); +``` + +**Good**: + +```javascript +var format = require( '@stdlib/string/format' ); + +var str = format( '%s %s', 'foo', 'bar' ); +// returns 'foo bar' +``` + +
+ + + +
+ +## Notes + +- The rule only applies when the format function is resolved from a `require( '@stdlib/string/format' )` statement, thus guarding against unrelated symbols sharing the same name and against shadowed symbols. +- The rule skips calls in which the format string is not a string literal, calls containing spread elements, calls via `#.call` and `#.apply`, and format strings containing positional placeholders (e.g., `%1$s`). + +
+ + + +
+ +## Examples + +```javascript +var Linter = require( 'eslint' ).Linter; +var rule = require( '@stdlib/_tools/eslint/rules/format-args' ); + +var linter = new Linter(); + +var code = [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'var str = format( \'%s %s\', \'foo\' );' +].join( '\n' ); + +linter.defineRule( 'format-args', rule ); + +var result = linter.verify( code, { + 'rules': { + 'format-args': 'error' + } +}); +/* returns + [ + { + 'ruleId': 'format-args', + 'severity': 2, + 'message': 'Unexpected number of format arguments. Format string expects 2 argument(s), but 1 argument(s) were provided.', + 'line': 3, + 'column': 11, + 'nodeType': 'CallExpression', + 'endLine': 3, + 'endColumn': 35 + } + ] +*/ +``` + +
+ + + + + + + + + + + + + + diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/examples/index.js b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/examples/index.js new file mode 100644 index 000000000000..7426e1feebdd --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/examples/index.js @@ -0,0 +1,52 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2026 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 rule = require( './../lib' ); + +var linter = new Linter(); +var code = [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'var str = format( \'%s %s\', \'foo\' );' +].join( '\n' ); + +linter.defineRule( 'format-args', rule ); + +var results = linter.verify( code, { + 'rules': { + 'format-args': 'error' + } +}); +console.log( results ); +/* => + [ + { + 'ruleId': 'format-args', + 'severity': 2, + 'message': 'Unexpected number of format arguments. Format string expects 2 argument(s), but 1 argument(s) were provided.', + 'line': 3, + 'column': 11, + 'nodeType': 'CallExpression', + 'endLine': 3, + 'endColumn': 35 + } + ] +*/ diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/lib/index.js b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/lib/index.js new file mode 100644 index 000000000000..3a8300dc0481 --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/lib/index.js @@ -0,0 +1,39 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2026 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 requiring that format calls are provided an expected number of arguments. +* +* @module @stdlib/_tools/eslint/rules/format-args +* +* @example +* var rule = require( '@stdlib/_tools/eslint/rules/format-args' ); +* +* console.log( rule ); +*/ + +// MODULES // + +var main = require( './main.js' ); + + +// EXPORTS // + +module.exports = main; diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/lib/main.js b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/lib/main.js new file mode 100644 index 000000000000..a8eb6cc7a13d --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/lib/main.js @@ -0,0 +1,203 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2026 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 formatTokenize = require( '@stdlib/string/base/format-tokenize' ); + + +// VARIABLES // + +var PKG = '@stdlib/string/format'; +var rule; + + +// FUNCTIONS // + +/** +* Returns a boolean indicating whether a provided identifier name resolves to the format function exported by `@stdlib/string/format`. +* +* ## Notes +* +* - The function walks parent scopes and inspects the **nearest** binding for the provided name, thus guarding against shadowed symbols. +* +* @private +* @param {Object} scope - scope +* @param {string} name - identifier name +* @returns {boolean} boolean indicating whether an identifier name resolves to the format function +*/ +function isFormat( scope, name ) { + var v; + + while ( scope ) { + if ( scope.set.has( name ) ) { + v = scope.set.get( name ); + if ( + v.defs.length > 0 && + v.defs[ 0 ].node.type === 'VariableDeclarator' && + v.defs[ 0 ].node.init && + v.defs[ 0 ].node.init.type === 'CallExpression' && + v.defs[ 0 ].node.init.callee.name === 'require' && + v.defs[ 0 ].node.init.arguments.length > 0 && + v.defs[ 0 ].node.init.arguments[ 0 ].value === PKG + ) { + return true; + } + return false; + } + scope = scope.upper; + } + return false; +} + +/** +* Resolves the number of arguments expected by a format string. +* +* ## Notes +* +* - The function returns `-1` if a format string contains positional placeholders (e.g., `%1$s`), as the number of expected arguments cannot be resolved by simply counting placeholders. +* +* @private +* @param {string} str - format string +* @returns {integer} number of expected arguments or `-1` +*/ +function numArgs( str ) { + var tokens; + var count; + var tok; + var i; + + tokens = formatTokenize( str ); + count = 0; + for ( i = 0; i < tokens.length; i++ ) { + tok = tokens[ i ]; + + // Plain (non-placeholder) string parts (including the `%` resulting from an escaped `%%`) do not consume arguments... + if ( typeof tok === 'string' ) { + continue; + } + // Bail on positional placeholders (e.g., `%1$s`), as arguments may be consumed out-of-order and/or reused: + if ( tok.mapping !== void 0 ) { + return -1; + } + count += 1; + + // Dynamic widths and precisions (e.g., `%*d` and `%.*f`) each consume an additional argument... + if ( tok.width === '*' ) { + count += 1; + } + if ( tok.precision === '*' ) { + count += 1; + } + } + return count; +} + +/** +* Rule for requiring that format calls are provided an expected number of arguments. +* +* @param {Object} context - ESLint context +* @returns {Object} validators +*/ +function main( context ) { + var source = context.sourceCode; + + /** + * Reports the error message. + * + * @private + * @param {ASTNode} node - node to report + * @param {integer} expected - number of expected arguments + * @param {integer} actual - number of provided arguments + */ + function report( node, expected, actual ) { + context.report({ + 'node': node, + 'message': 'Unexpected number of format arguments. Format string expects ' + expected + ' argument(s), but ' + actual + ' argument(s) were provided.' + }); + } + + /** + * Validates a call expression. + * + * @private + * @param {ASTNode} node - node to examine + */ + function validate( node ) { + var expected; + var args; + var i; + + // Note: member expression callees (e.g., `format.call( ... )` and `format.apply( ... )`) are intentionally skipped, as the callee is not an `Identifier` node... + if ( + node.callee === void 0 || + node.callee.type !== 'Identifier' || + !isFormat( source.getScope( node ), node.callee.name ) + ) { + return; + } + args = node.arguments; + + // Only check calls in which the format string is a string literal... + if ( + args.length === 0 || + args[ 0 ].type !== 'Literal' || + typeof args[ 0 ].value !== 'string' + ) { + return; + } + // Skip calls containing spread elements, as the number of provided arguments cannot be statically resolved... + for ( i = 1; i < args.length; i++ ) { + if ( args[ i ].type === 'SpreadElement' ) { + return; + } + } + expected = numArgs( args[ 0 ].value ); + if ( expected === -1 ) { + return; + } + if ( args.length-1 !== expected ) { + report( node, expected, args.length-1 ); + } + } + + return { + 'CallExpression': validate + }; +} + + +// MAIN // + +rule = { + 'meta': { + 'type': 'problem', + 'docs': { + 'description': 'require that format calls are provided an expected number of arguments' + }, + 'schema': [] + }, + 'create': main +}; + + +// EXPORTS // + +module.exports = rule; diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/package.json b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/package.json new file mode 100644 index 000000000000..35e9c6a1d95b --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/package.json @@ -0,0 +1,54 @@ +{ + "name": "@stdlib/_tools/eslint/rules/format-args", + "version": "0.0.0", + "description": "ESLint rule requiring that format calls are provided an expected number of arguments.", + "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" + } + ], + "bin": {}, + "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", + "rules", + "rule", + "plugin", + "format", + "interpolate", + "string", + "arguments", + "placeholders" + ] +} diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/invalid.js b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/invalid.js new file mode 100644 index 000000000000..033d00884fed --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/invalid.js @@ -0,0 +1,141 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2026 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 invalid = []; +var test; + +// Too few arguments... +test = { + 'code': [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'var str = format( \'%s %s\', \'foo\' );' + ].join( '\n' ), + 'errors': [ + { + 'message': 'Unexpected number of format arguments. Format string expects 2 argument(s), but 1 argument(s) were provided.', + 'type': 'CallExpression' + } + ] +}; +invalid.push( test ); + +// Too many arguments... +test = { + 'code': [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'var str = format( \'%s %s\', \'foo\', \'bar\', \'beep\' );' + ].join( '\n' ), + 'errors': [ + { + 'message': 'Unexpected number of format arguments. Format string expects 2 argument(s), but 3 argument(s) were provided.', + 'type': 'CallExpression' + } + ] +}; +invalid.push( test ); + +// Missing all arguments... +test = { + 'code': [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'var str = format( \'%s\' );' + ].join( '\n' ), + 'errors': [ + { + 'message': 'Unexpected number of format arguments. Format string expects 1 argument(s), but 0 argument(s) were provided.', + 'type': 'CallExpression' + } + ] +}; +invalid.push( test ); + +// Nested call expressions... +test = { + 'code': [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'throw new Error( format( \'invalid argument. Must be a string. Value: `%s`.\' ) );' + ].join( '\n' ), + 'errors': [ + { + 'message': 'Unexpected number of format arguments. Format string expects 1 argument(s), but 0 argument(s) were provided.', + 'type': 'CallExpression' + } + ] +}; +invalid.push( test ); + +// Aliases other than `format`... +test = { + 'code': [ + 'var fmt = require( \'@stdlib/string/format\' );', + '', + 'var str = fmt( \'%s %s\', \'foo\' );' + ].join( '\n' ), + 'errors': [ + { + 'message': 'Unexpected number of format arguments. Format string expects 2 argument(s), but 1 argument(s) were provided.', + 'type': 'CallExpression' + } + ] +}; +invalid.push( test ); + +// Usage in nested scopes resolving to an outer require... +test = { + 'code': [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'function foo( x ) {', + ' return format( \'%s %s\', x );', + '}' + ].join( '\n' ), + 'errors': [ + { + 'message': 'Unexpected number of format arguments. Format string expects 2 argument(s), but 1 argument(s) were provided.', + 'type': 'CallExpression' + } + ] +}; +invalid.push( test ); + +// Dynamic widths consume an additional argument... +test = { + 'code': [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'var str = format( \'%*d\', 10 );' + ].join( '\n' ), + 'errors': [ + { + 'message': 'Unexpected number of format arguments. Format string expects 2 argument(s), but 1 argument(s) were provided.', + 'type': 'CallExpression' + } + ] +}; +invalid.push( test ); + + +// EXPORTS // + +module.exports = invalid; diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/unvalidated.js b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/unvalidated.js new file mode 100644 index 000000000000..4ced508bc74b --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/unvalidated.js @@ -0,0 +1,57 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2026 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 unvalidated = []; +var test; + +test = { + 'code': [ + 'var x = 123;' + ].join( '\n' ) +}; +unvalidated.push( test ); + +test = { + 'code': [ + 'var str = format( \'%s %s\', \'foo\' );' + ].join( '\n' ) +}; +unvalidated.push( test ); + +test = { + 'code': [ + 'var format = require( \'@stdlib/string/format\' );' + ].join( '\n' ) +}; +unvalidated.push( test ); + +test = { + 'code': [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'var str = format();' + ].join( '\n' ) +}; +unvalidated.push( test ); + + +// EXPORTS // + +module.exports = unvalidated; diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/valid.js b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/valid.js new file mode 100644 index 000000000000..0495b3c8d9da --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/valid.js @@ -0,0 +1,182 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2026 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 valid = []; +var test; + +test = { + 'code': [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'var str = format( \'%s %s\', \'foo\', \'bar\' );' + ].join( '\n' ) +}; +valid.push( test ); + +test = { + 'code': [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'var str = format( \'beep\' );' + ].join( '\n' ) +}; +valid.push( test ); + +test = { + 'code': [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'throw new Error( format( \'invalid argument. Must provide a string. Value: `%s`.\', \'foo\' ) );' + ].join( '\n' ) +}; +valid.push( test ); + +// Escaped percent signs do not consume arguments... +test = { + 'code': [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'var str = format( \'%d%%\', 10 );' + ].join( '\n' ) +}; +valid.push( test ); + +// Dynamic widths and precisions each consume an additional argument... +test = { + 'code': [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'var str = format( \'%*d\', 5, 10 );' + ].join( '\n' ) +}; +valid.push( test ); + +test = { + 'code': [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'var str = format( \'%.*f\', 2, 3.14159 );' + ].join( '\n' ) +}; +valid.push( test ); + +// Positional placeholders are skipped... +test = { + 'code': [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'var str = format( \'%1$s %1$s\', \'foo\' );' + ].join( '\n' ) +}; +valid.push( test ); + +// Non-literal format strings are skipped... +test = { + 'code': [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'var str = \'%s %s\';', + 'var out = format( str, \'foo\' );' + ].join( '\n' ) +}; +valid.push( test ); + +// Spread elements are skipped... +test = { + 'code': [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'var args = [ \'foo\' ];', + 'var str = format( \'%s %s\', ...args );' + ].join( '\n' ), + 'parserOptions': { + 'ecmaVersion': 2018 + } +}; +valid.push( test ); + +// Calls via `#.call` and `#.apply` are skipped... +test = { + 'code': [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'var str = format.call( null, \'%s %s\', \'foo\' );' + ].join( '\n' ) +}; +valid.push( test ); + +test = { + 'code': [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'var str = format.apply( null, [ \'%s %s\', \'foo\' ] );' + ].join( '\n' ) +}; +valid.push( test ); + +// Shadowed symbols are not flagged... +test = { + 'code': [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'function foo( format ) {', + ' return format( \'%s %s\', \'foo\' );', + '}' + ].join( '\n' ) +}; +valid.push( test ); + +test = { + 'code': [ + 'var format = require( \'@stdlib/string/format\' );', + '', + 'function foo() {', + ' var format = require( \'d3-format\' );', + ' return format( \'%s %s\', \'foo\' );', + '}' + ].join( '\n' ) +}; +valid.push( test ); + +// Unrelated symbols sharing the same name are not flagged... +test = { + 'code': [ + 'var format = require( \'d3-format\' );', + '', + 'var str = format( \'%s %s\', \'foo\' );' + ].join( '\n' ) +}; +valid.push( test ); + +test = { + 'code': [ + 'function format( str ) {', + ' return str;', + '}', + '', + 'var str = format( \'%s %s\', \'foo\' );' + ].join( '\n' ) +}; +valid.push( test ); + + +// EXPORTS // + +module.exports = valid; diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/test.js b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/test.js new file mode 100644 index 000000000000..38346420ddd9 --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/test.js @@ -0,0 +1,98 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2026 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 format calls which are provided an expected number of arguments', function test( t ) { + var tester = new RuleTester({ + 'parserOptions': { + 'ecmaVersion': 2020 + } + }); + + try { + tester.run( 'format-args', 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 format calls which are provided an unexpected number of arguments', function test( t ) { + var tester = new RuleTester({ + 'parserOptions': { + 'ecmaVersion': 2020 + } + }); + + try { + tester.run( 'format-args', 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 code not containing checkable format calls', function test( t ) { + var tester = new RuleTester({ + 'parserOptions': { + 'ecmaVersion': 2020 + } + }); + + try { + tester.run( 'format-args', rule, { + 'valid': unvalidated, + 'invalid': [] + }); + t.pass( 'passed without errors' ); + } catch ( err ) { + t.fail( 'encountered an error: ' + err.message ); + } + t.end(); +}); 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 c90cab4a4289..6b5328c148d4 100644 --- a/lib/node_modules/@stdlib/_tools/eslint/rules/lib/index.js +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/lib/index.js @@ -108,6 +108,15 @@ setReadOnly( rules, 'eol-open-bracket-spacing', require( '@stdlib/_tools/eslint/ */ setReadOnly( rules, 'first-unit-test', require( '@stdlib/_tools/eslint/rules/first-unit-test' ) ); +/** +* @name format-args +* @memberof rules +* @readonly +* @type {Function} +* @see {@link module:@stdlib/_tools/eslint/rules/format-args} +*/ +setReadOnly( rules, 'format-args', require( '@stdlib/_tools/eslint/rules/format-args' ) ); + /** * @name jsdoc-blockquote-indentation * @memberof rules From c856203b612f5fb5a88edaf2cb1c675962e1f745 Mon Sep 17 00:00:00 2001 From: Philipp Burckhardt Date: Sat, 22 Aug 2026 00:59:08 -0500 Subject: [PATCH 2/4] chore: add `os` field and lint annotation per review Co-Authored-By: Claude Fable 5 --- 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_pkg_readmes status: passed - task: lint_markdown_docs status: na - task: lint_markdown status: na - task: lint_package_json status: passed - task: lint_repl_help status: na - task: lint_javascript_src status: na - task: lint_javascript_cli status: na - task: lint_javascript_examples status: na - task: lint_javascript_tests status: na - 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 --- --- .../@stdlib/_tools/eslint/rules/format-args/README.md | 2 ++ .../_tools/eslint/rules/format-args/package.json | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/README.md b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/README.md index 41247f14d2aa..ca129272e17f 100644 --- a/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/README.md +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/README.md @@ -78,6 +78,8 @@ var str = format( '%s %s', 'foo', 'bar' ); ## Examples + + ```javascript var Linter = require( 'eslint' ).Linter; var rule = require( '@stdlib/_tools/eslint/rules/format-args' ); diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/package.json b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/package.json index 35e9c6a1d95b..ab0e31a7c94f 100644 --- a/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/package.json +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/package.json @@ -35,6 +35,17 @@ "node": ">=0.10.0", "npm": ">2.7.0" }, + "os": [ + "aix", + "darwin", + "freebsd", + "linux", + "macos", + "openbsd", + "sunos", + "win32", + "windows" + ], "keywords": [ "stdlib", "tools", From 0aac681688473440deaabb11a32c09be1accc9ee Mon Sep 17 00:00:00 2001 From: Athan Date: Sat, 22 Aug 2026 01:07:02 -0700 Subject: [PATCH 3/4] refactor: use assertion utility Signed-off-by: Athan --- .../@stdlib/_tools/eslint/rules/format-args/lib/main.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/lib/main.js b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/lib/main.js index a8eb6cc7a13d..8712f5f8d1db 100644 --- a/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/lib/main.js +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/lib/main.js @@ -20,6 +20,7 @@ // MODULES // +var isString = require( '@stdlib/assert/is-string' ).isPrimitive; var formatTokenize = require( '@stdlib/string/base/format-tokenize' ); @@ -78,7 +79,7 @@ function isFormat( scope, name ) { * @param {string} str - format string * @returns {integer} number of expected arguments or `-1` */ -function numArgs( str ) { +function argumentCount( str ) { var tokens; var count; var tok; @@ -90,7 +91,7 @@ function numArgs( str ) { tok = tokens[ i ]; // Plain (non-placeholder) string parts (including the `%` resulting from an escaped `%%`) do not consume arguments... - if ( typeof tok === 'string' ) { + if ( isString( tok ) ) { continue; } // Bail on positional placeholders (e.g., `%1$s`), as arguments may be consumed out-of-order and/or reused: @@ -159,7 +160,7 @@ function main( context ) { if ( args.length === 0 || args[ 0 ].type !== 'Literal' || - typeof args[ 0 ].value !== 'string' + !isString( args[ 0 ].value ) ) { return; } @@ -169,7 +170,7 @@ function main( context ) { return; } } - expected = numArgs( args[ 0 ].value ); + expected = argumentCount( args[ 0 ].value ); if ( expected === -1 ) { return; } From 9462818a67b130a2ea92fe2f1d28299b7412f042 Mon Sep 17 00:00:00 2001 From: Athan Date: Sat, 22 Aug 2026 01:08:33 -0700 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Athan Signed-off-by: Athan --- .../_tools/eslint/rules/format-args/test/fixtures/invalid.js | 3 +-- .../eslint/rules/format-args/test/fixtures/unvalidated.js | 3 +-- .../_tools/eslint/rules/format-args/test/fixtures/valid.js | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/invalid.js b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/invalid.js index 033d00884fed..179317e306d1 100644 --- a/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/invalid.js +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/invalid.js @@ -19,10 +19,9 @@ 'use strict'; var invalid = []; -var test; // Too few arguments... -test = { +var test = { 'code': [ 'var format = require( \'@stdlib/string/format\' );', '', diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/unvalidated.js b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/unvalidated.js index 4ced508bc74b..65a0b80fa0e0 100644 --- a/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/unvalidated.js +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/unvalidated.js @@ -19,9 +19,8 @@ 'use strict'; var unvalidated = []; -var test; -test = { +var test = { 'code': [ 'var x = 123;' ].join( '\n' ) diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/valid.js b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/valid.js index 0495b3c8d9da..bd1ce1ce75fc 100644 --- a/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/valid.js +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/format-args/test/fixtures/valid.js @@ -19,9 +19,8 @@ 'use strict'; var valid = []; -var test; -test = { +var test = { 'code': [ 'var format = require( \'@stdlib/string/format\' );', '',