From 54ac4fd4c5cf30c0f4c239e1066fb693ab7349fc Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sun, 23 Jun 2024 11:57:36 +0800 Subject: [PATCH] fix(`no-undefined-types`): treat variables imported by `@import` tags as defined; fixes #1244 --- .README/rules/no-undefined-types.md | 2 +- docs/rules/no-undefined-types.md | 69 ++++++++++++++++++- package.json | 1 + pnpm-lock.yaml | 22 ++++++ src/rules/noUndefinedTypes.js | 59 ++++++++++++++-- test/rules/assertions/noUndefinedTypes.js | 83 +++++++++++++++++++++++ 6 files changed, 229 insertions(+), 7 deletions(-) diff --git a/.README/rules/no-undefined-types.md b/.README/rules/no-undefined-types.md index 3d856f206..56fa10315 100644 --- a/.README/rules/no-undefined-types.md +++ b/.README/rules/no-undefined-types.md @@ -17,7 +17,7 @@ the tag types in the table below: `@event`, `@external` (or `@host`), `@function` (or `@func` or `@method`), `@interface`, `@member` (or `@var`), `@mixin`, `@name`, `@namespace`, `@template` (for "closure" or "typescript" `settings.jsdoc.mode` only), -`@typedef`. +`@import` (for TypeScript), `@typedef`. The following tags will also be checked but only when the mode is `closure`: diff --git a/docs/rules/no-undefined-types.md b/docs/rules/no-undefined-types.md index 06573cc27..6c7e55732 100644 --- a/docs/rules/no-undefined-types.md +++ b/docs/rules/no-undefined-types.md @@ -23,7 +23,7 @@ the tag types in the table below: `@event`, `@external` (or `@host`), `@function` (or `@func` or `@method`), `@interface`, `@member` (or `@var`), `@mixin`, `@name`, `@namespace`, `@template` (for "closure" or "typescript" `settings.jsdoc.mode` only), -`@typedef`. +`@import` (for TypeScript), `@typedef`. The following tags will also be checked but only when the mode is `closure`: @@ -307,6 +307,36 @@ const a = new Todo(); */ // Settings: {"jsdoc":{"structuredTags":{"namepathOrURLReferencer":{"name":"namepath-or-url-referencing"}}}} // Message: The type 'SomeType' is undefined. + +/** + * @import BadImportIgnoredByThisRule + */ +/** + * @import LinterDef, { Sth as Something, Another as Another2 } from "eslint" + */ +/** + * @import { Linter } from "eslint" + */ +/** + * @import LinterDefault from "eslint" + */ +/** + * @import {Linter as Lintee} from "eslint" + */ +/** + * @import * as Linters from "eslint" + */ + +/** + * @type {BadImportIgnoredByThisRule} + */ +/** + * @type {Sth} + */ +/** + * @type {Another} + */ +// Message: The type 'BadImportIgnoredByThisRule' is undefined. ```` @@ -777,8 +807,45 @@ function quux(foo) { quux(0); +/** + * @import BadImportIgnoredByThisRule + */ +/** + * @import LinterDef, { Sth as Something, Another as Another2 } from "eslint" + */ /** * @import { Linter } from "eslint" */ +/** + * @import LinterDefault from "eslint" + */ +/** + * @import {Linter as Lintee} from "eslint" + */ +/** + * @import * as Linters from "eslint" + */ + +/** + * @type {LinterDef} + */ +/** + * @type {Something} + */ +/** + * @type {Another2} + */ +/** + * @type {Linter} + */ +/** + * @type {LinterDefault} + */ +/** + * @type {Lintee} + */ +/** + * @type {Linters} + */ ```` diff --git a/package.json b/package.json index 2a47cd052..e482610f9 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "esquery": "^1.5.0", + "parse-imports": "^2.0.0", "semver": "^7.6.2", "spdx-expression-parse": "^4.0.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28bf266b2..2462ab7c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: esquery: specifier: ^1.5.0 version: 1.5.0 + parse-imports: + specifier: ^2.0.0 + version: 2.0.0 semver: specifier: ^7.6.2 version: 7.6.2 @@ -2363,6 +2366,9 @@ packages: resolution: {integrity: sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==} engines: {node: '>= 0.4'} + es-module-lexer@1.5.3: + resolution: {integrity: sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==} + es-object-atoms@1.0.0: resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} engines: {node: '>= 0.4'} @@ -4412,6 +4418,10 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-imports@2.0.0: + resolution: {integrity: sha512-3Dh6W2SPvs4k8neQG6fluUnVgt/WLDheepyD+FP1GlmOijKRBqr7Vhij0Cko6wyAp3tm/OHDDAniwFS0VCA0qQ==} + engines: {node: '>= 18'} + parse-json@4.0.0: resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} engines: {node: '>=4'} @@ -4975,6 +4985,9 @@ packages: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} + slashes@3.0.12: + resolution: {integrity: sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==} + slice-ansi@5.0.0: resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} engines: {node: '>=12'} @@ -8560,6 +8573,8 @@ snapshots: iterator.prototype: 1.1.2 safe-array-concat: 1.1.2 + es-module-lexer@1.5.3: {} + es-object-atoms@1.0.0: dependencies: es-errors: 1.3.0 @@ -10878,6 +10893,11 @@ snapshots: dependencies: callsites: 3.1.0 + parse-imports@2.0.0: + dependencies: + es-module-lexer: 1.5.3 + slashes: 3.0.12 + parse-json@4.0.0: dependencies: error-ex: 1.3.2 @@ -11470,6 +11490,8 @@ snapshots: slash@5.1.0: {} + slashes@3.0.12: {} + slice-ansi@5.0.0: dependencies: ansi-styles: 6.2.1 diff --git a/src/rules/noUndefinedTypes.js b/src/rules/noUndefinedTypes.js index 9e8300c4f..c84ed43bb 100644 --- a/src/rules/noUndefinedTypes.js +++ b/src/rules/noUndefinedTypes.js @@ -1,12 +1,13 @@ -import iterateJsdoc, { - parseComment, -} from '../iterateJsdoc.js'; +import { parseImportsSync } from 'parse-imports'; import { getJSDocComment, parse as parseType, traverse, tryParse as tryParseType, } from '@es-joy/jsdoccomment'; +import iterateJsdoc, { + parseComment, +} from '../iterateJsdoc.js'; const extraTypes = [ 'null', 'undefined', 'void', 'string', 'boolean', 'object', @@ -109,13 +110,15 @@ export default iterateJsdoc(({ .filter(Boolean)); } - const typedefDeclarations = sourceCode.getAllComments() + const comments = sourceCode.getAllComments() .filter((comment) => { return (/^\*\s/u).test(comment.value); }) .map((commentNode) => { return parseComment(commentNode, ''); - }) + }); + + const typedefDeclarations = comments .flatMap((doc) => { return doc.tags.filter(({ tag, @@ -127,6 +130,51 @@ export default iterateJsdoc(({ return tag.name; }); + + const importTags = settings.mode === 'typescript' ? /** @type {string[]} */ (comments.flatMap((doc) => { + return doc.tags.filter(({ + tag, + }) => { + return tag === 'import'; + }); + }).flatMap((tag) => { + const { + type, name, description + } = tag; + const typePart = type ? `{${type}} `: ''; + const imprt = 'import ' + (description + ? `${typePart}${name} ${description}` + : `${typePart}${name}`); + + let imports; + try { + // Should technically await non-sync, but ESLint doesn't support async rules; + // thankfully, the Wasm load time is safely fast + imports = parseImportsSync(imprt); + } catch (err) { + return null; + } + + return [...imports].flatMap(({importClause}) => { + /* c8 ignore next */ + const {default: dflt, named, namespace} = importClause || {}; + const types = []; + if (dflt) { + types.push(dflt); + } + if (namespace) { + types.push(namespace); + } + if (named) { + for (const {binding} of named) { + types.push(binding); + } + } + + return types; + }); + }).filter(Boolean)) : []; + const ancestorNodes = []; let currentNode = node; @@ -193,6 +241,7 @@ export default iterateJsdoc(({ ) .concat(extraTypes) .concat(typedefDeclarations) + .concat(importTags) .concat(definedTypes) .concat(/** @type {string[]} */ (definedPreferredTypes)) .concat( diff --git a/test/rules/assertions/noUndefinedTypes.js b/test/rules/assertions/noUndefinedTypes.js index a09cbb56e..44d3e68b2 100644 --- a/test/rules/assertions/noUndefinedTypes.js +++ b/test/rules/assertions/noUndefinedTypes.js @@ -558,6 +558,52 @@ export default { }, }, }, + { + code: ` + /** + * @import BadImportIgnoredByThisRule + */ + /** + * @import LinterDef, { Sth as Something, Another as Another2 } from "eslint" + */ + /** + * @import { Linter } from "eslint" + */ + /** + * @import LinterDefault from "eslint" + */ + /** + * @import {Linter as Lintee} from "eslint" + */ + /** + * @import * as Linters from "eslint" + */ + + /** + * @type {BadImportIgnoredByThisRule} + */ + /** + * @type {Sth} + */ + /** + * @type {Another} + */ + `, + errors: [ + { + line: 22, + message: 'The type \'BadImportIgnoredByThisRule\' is undefined.', + }, + { + line: 25, + message: 'The type \'Sth\' is undefined.', + }, + { + line: 28, + message: 'The type \'Another\' is undefined.', + }, + ], + }, ], valid: [ { @@ -1438,9 +1484,46 @@ export default { }, { code: ` + /** + * @import BadImportIgnoredByThisRule + */ + /** + * @import LinterDef, { Sth as Something, Another as Another2 } from "eslint" + */ /** * @import { Linter } from "eslint" */ + /** + * @import LinterDefault from "eslint" + */ + /** + * @import {Linter as Lintee} from "eslint" + */ + /** + * @import * as Linters from "eslint" + */ + + /** + * @type {LinterDef} + */ + /** + * @type {Something} + */ + /** + * @type {Another2} + */ + /** + * @type {Linter} + */ + /** + * @type {LinterDefault} + */ + /** + * @type {Lintee} + */ + /** + * @type {Linters} + */ `, }, ],