diff --git a/packages/comparisons/src/data.json b/packages/comparisons/src/data.json index 072745993..13fc05576 100644 --- a/packages/comparisons/src/data.json +++ b/packages/comparisons/src/data.json @@ -16106,7 +16106,8 @@ ], "flint": { "name": "restrictedImports", - "plugin": "ts" + "plugin": "ts", + "status": "implemented" }, "oxlint": [ { diff --git a/packages/site/src/content/docs/rules/ts/restrictedImports.mdx b/packages/site/src/content/docs/rules/ts/restrictedImports.mdx new file mode 100644 index 000000000..f76ae8654 --- /dev/null +++ b/packages/site/src/content/docs/rules/ts/restrictedImports.mdx @@ -0,0 +1,144 @@ +--- +description: "Restricts specified modules from being imported." +title: "restrictedImports" +topic: "rules" +--- + +import { Tabs, TabItem } from "@astrojs/starlight/components"; +import { RuleEquivalents } from "~/components/RuleEquivalents"; +import RuleSummary from "~/components/RuleSummary.astro"; + + + +Some modules should not be used in certain parts of a codebase. +This rule allows you to restrict specific modules from being imported, either by exact path or by glob pattern. +It detects static `import` and `export ... from` declarations. + +## Examples + + + + +```ts +// With paths: ["lodash"] +import _ from "lodash"; + +// With paths: [{ name: "utils", importNames: ["dangerousHelper"] }] +import { dangerousHelper } from "utils"; + +// With patterns: ["internal/*"] +import { secret } from "internal/auth"; +``` + + + + +```ts +// With paths: ["lodash"] +import { pick } from "lodash-es"; + +// With paths: [{ name: "utils", importNames: ["dangerousHelper"] }] +import { safeHelper } from "utils"; + +// With patterns: ["internal/*"] +import { auth } from "external/auth"; +``` + + + + +## Options + +### `paths` + +- Type: `Array` +- Default: `[]` + +Exact module specifiers to restrict. +Each entry can be a simple string or a configuration object. + +```jsonc +{ + "paths": [ + // Simple: restrict the entire module + "lodash", + + // Advanced: restrict specific imports + { + "name": "utils", + "importNames": ["dangerousHelper"], + "message": "Use safeHelper instead.", + }, + + // Allow only certain imports + { + "name": "react", + "allowImportNames": ["useState", "useEffect"], + }, + + // Allow type-only imports + { + "name": "internal-types", + "allowTypeImports": true, + }, + ], +} +``` + +**PathConfig fields:** + +| Field | Type | Description | +| ------------------ | ----------- | --------------------------------------------------------------- | +| `name` | `string` | The module specifier to restrict. | +| `message` | `string?` | Custom message shown when the import is restricted. | +| `importNames` | `string[]?` | Only these specific import names are restricted. | +| `allowImportNames` | `string[]?` | Only these import names are allowed; all others are restricted. | +| `allowTypeImports` | `boolean?` | Whether type-only imports are exempt from the restriction. | + +### `patterns` + +- Type: `Array` +- Default: `[]` + +Glob patterns to match restricted module specifiers. + +```jsonc +{ + "patterns": [ + // Simple: restrict by glob + "internal/*", + + // Advanced: restrict with options + { + "group": ["legacy/**", "deprecated/**"], + "message": "These modules are deprecated. Use the new API.", + "allowTypeImports": true, + }, + ], +} +``` + +**PatternConfig fields:** + +| Field | Type | Description | +| ------------------ | ----------- | --------------------------------------------------------------- | +| `group` | `string[]` | Glob patterns to match module specifiers against. | +| `message` | `string?` | Custom message shown when a matching import is restricted. | +| `importNames` | `string[]?` | Only these specific import names are restricted. | +| `allowImportNames` | `string[]?` | Only these import names are allowed; all others are restricted. | +| `allowTypeImports` | `boolean?` | Whether type-only imports are exempt from the restriction. | + +## When Not To Use It + +If you do not need to restrict any modules from being imported, you do not need this rule. +This rule requires explicit configuration to do anything, so it has no effect without `paths` or `patterns` options. +You might consider using [Flint disable comments](/directives) and/or [configuration file disables](/configuration#files) for specific situations instead of completely disabling this rule. + +## Further Reading + +- [ESLint `no-restricted-imports`](https://eslint.org/docs/latest/rules/no-restricted-imports) +- [TypeScript-ESLint `no-restricted-imports`](https://typescript-eslint.io/rules/no-restricted-imports) + +## Equivalents in Other Linters + + diff --git a/packages/ts/src/plugin.ts b/packages/ts/src/plugin.ts index 56f9f18bf..c151ee77f 100644 --- a/packages/ts/src/plugin.ts +++ b/packages/ts/src/plugin.ts @@ -241,6 +241,7 @@ import regexUnusedLazyQuantifiers from "./rules/regexUnusedLazyQuantifiers.ts"; import regexUnusedQuantifiers from "./rules/regexUnusedQuantifiers.ts"; import regexValidity from "./rules/regexValidity.ts"; import regexWordMatchers from "./rules/regexWordMatchers.ts"; +import restrictedImports from "./rules/restrictedImports.ts"; import returnAssignments from "./rules/returnAssignments.ts"; import selfAssignments from "./rules/selfAssignments.ts"; import sequences from "./rules/sequences.ts"; @@ -527,6 +528,7 @@ export const ts = createPlugin({ regexUnusedQuantifiers, regexValidity, regexWordMatchers, + restrictedImports, returnAssignments, selfAssignments, sequences, diff --git a/packages/ts/src/rules/restrictedImports.test.ts b/packages/ts/src/rules/restrictedImports.test.ts new file mode 100644 index 000000000..32729b2ba --- /dev/null +++ b/packages/ts/src/rules/restrictedImports.test.ts @@ -0,0 +1,390 @@ +import rule from "./restrictedImports.ts"; +import { ruleTester } from "./ruleTester.ts"; + +ruleTester.describe(rule, { + invalid: [ + { + code: ` +import foo from "forbidden"; +`, + options: { + paths: ["forbidden"], + }, + snapshot: ` +import foo from "forbidden"; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +'forbidden' import is restricted from being used. +`, + }, + { + code: ` +import foo from "forbidden"; +`, + options: { + paths: [ + { + message: "Use 'allowed-mod' instead.", + name: "forbidden", + }, + ], + }, + snapshot: ` +import foo from "forbidden"; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +'forbidden' import is restricted from being used. Use 'allowed-mod' instead. +`, + }, + { + code: ` +import { badExport } from "mod"; +`, + options: { + paths: [ + { + importNames: ["badExport"], + name: "mod", + }, + ], + }, + snapshot: ` +import { badExport } from "mod"; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +'badExport' import from 'mod' is restricted. +`, + }, + { + code: ` +import foo from "mod"; +`, + options: { + paths: [ + { + importNames: ["default"], + name: "mod", + }, + ], + }, + snapshot: ` +import foo from "mod"; +~~~~~~~~~~~~~~~~~~~~~~ +'default' import from 'mod' is restricted. +`, + }, + { + code: ` +import * as ns from "mod"; +`, + options: { + paths: [ + { + importNames: ["badExport"], + name: "mod", + }, + ], + }, + snapshot: ` +import * as ns from "mod"; +~~~~~~~~~~~~~~~~~~~~~~~~~~ +* import is invalid because 'badExport' from 'mod' is restricted. +`, + }, + { + code: ` +import "forbidden"; +`, + options: { + paths: ["forbidden"], + }, + snapshot: ` +import "forbidden"; +~~~~~~~~~~~~~~~~~~~ +'forbidden' import is restricted from being used. +`, + }, + { + code: ` +import { notAllowed } from "mod"; +`, + options: { + paths: [ + { + allowImportNames: ["allowed"], + name: "mod", + }, + ], + }, + snapshot: ` +import { notAllowed } from "mod"; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +'notAllowed' import from 'mod' is restricted. +`, + }, + { + code: ` +import foo from "internal/secret"; +`, + options: { + patterns: ["internal/*"], + }, + snapshot: ` +import foo from "internal/secret"; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +'internal/secret' import is restricted from being used by a pattern. +`, + }, + { + code: ` +import foo from "internal/secret"; +`, + options: { + patterns: [ + { + group: ["internal/*"], + message: "Do not import from internal modules.", + }, + ], + }, + snapshot: ` +import foo from "internal/secret"; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +'internal/secret' import is restricted from being used by a pattern. Do not import from internal modules. +`, + }, + { + code: ` +import { badName } from "utils/helpers"; +`, + options: { + patterns: [ + { + group: ["utils/*"], + importNames: ["badName"], + }, + ], + }, + snapshot: ` +import { badName } from "utils/helpers"; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +'badName' import from 'utils/helpers' is restricted. +`, + }, + { + code: ` +export { foo } from "forbidden"; +`, + options: { + paths: ["forbidden"], + }, + snapshot: ` +export { foo } from "forbidden"; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +'forbidden' import is restricted from being used. +`, + }, + { + code: ` +export * from "forbidden"; +`, + options: { + paths: ["forbidden"], + }, + snapshot: ` +export * from "forbidden"; +~~~~~~~~~~~~~~~~~~~~~~~~~~ +'forbidden' import is restricted from being used. +`, + }, + { + code: ` +import { foo } from "mod"; +`, + options: { + paths: [ + { + allowTypeImports: true, + name: "mod", + }, + ], + }, + snapshot: ` +import { foo } from "mod"; +~~~~~~~~~~~~~~~~~~~~~~~~~~ +'mod' import is restricted from being used. +`, + }, + { + code: ` +import { type A, b } from "mod"; +`, + options: { + paths: [ + { + allowTypeImports: true, + importNames: ["A", "b"], + name: "mod", + }, + ], + }, + snapshot: ` +import { type A, b } from "mod"; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +'b' import from 'mod' is restricted. +`, + }, + { + code: ` +import { badExport } from "mod"; +`, + options: { + paths: [ + { + importNames: ["badExport"], + message: "Use goodExport instead.", + name: "mod", + }, + ], + }, + snapshot: ` +import { badExport } from "mod"; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +'badExport' import from 'mod' is restricted. Use goodExport instead. +`, + }, + { + code: ` +import * as ns from "mod"; +`, + options: { + paths: [ + { + importNames: ["a", "b"], + message: "Import specific allowed names.", + name: "mod", + }, + ], + }, + snapshot: ` +import * as ns from "mod"; +~~~~~~~~~~~~~~~~~~~~~~~~~~ +* import is invalid because 'a', 'b' from 'mod' is restricted. Import specific allowed names. +`, + }, + { + code: ` +export * from "mod"; +`, + options: { + paths: [ + { + importNames: ["secret"], + name: "mod", + }, + ], + }, + snapshot: ` +export * from "mod"; +~~~~~~~~~~~~~~~~~~~~ +* import is invalid because 'secret' from 'mod' is restricted. +`, + }, + ], + valid: [ + { + code: `import foo from "allowed";`, + options: { + paths: ["forbidden"], + }, + }, + { + code: `import type { Foo } from "mod";`, + options: { + paths: [ + { + allowTypeImports: true, + name: "mod", + }, + ], + }, + }, + { + code: `import { allowed } from "mod";`, + options: { + paths: [ + { + allowImportNames: ["allowed"], + name: "mod", + }, + ], + }, + }, + { + code: `import { goodExport } from "mod";`, + options: { + paths: [ + { + importNames: ["badExport"], + name: "mod", + }, + ], + }, + }, + { + code: `import foo from "external/lib";`, + options: { + patterns: ["internal/*"], + }, + }, + `import foo from "anything";`, + { + code: `import "mod";`, + options: { + paths: [ + { + importNames: ["badExport"], + name: "mod", + }, + ], + }, + }, + { + code: `import { type A } from "mod";`, + options: { + paths: [ + { + allowTypeImports: true, + importNames: ["A"], + name: "mod", + }, + ], + }, + }, + { + code: `const foo = 1; export { foo };`, + options: { + paths: ["forbidden"], + }, + }, + { + code: `import { allowed } from "utils/helpers";`, + options: { + patterns: [ + { + allowImportNames: ["allowed"], + group: ["utils/*"], + }, + ], + }, + }, + { + code: `export type { Foo } from "mod";`, + options: { + paths: [ + { + allowTypeImports: true, + name: "mod", + }, + ], + }, + }, + ], +}); diff --git a/packages/ts/src/rules/restrictedImports.ts b/packages/ts/src/rules/restrictedImports.ts new file mode 100644 index 000000000..5301118b8 --- /dev/null +++ b/packages/ts/src/rules/restrictedImports.ts @@ -0,0 +1,523 @@ +import { + getTSNodeRange, + typescriptLanguage, +} from "@flint.fyi/typescript-language"; +import ts, { SyntaxKind } from "typescript"; +import { z } from "zod"; + +import { ruleCreator } from "./ruleCreator.ts"; + +const pathConfigSchema = z.object({ + allowImportNames: z + .array(z.string()) + .optional() + .describe("Import names that are explicitly allowed from this module."), + allowTypeImports: z + .boolean() + .optional() + .describe("Whether type-only imports from this module are allowed."), + importNames: z + .array(z.string()) + .optional() + .describe("Specific import names to restrict from this module."), + message: z + .string() + .optional() + .describe("A custom message to display when this module is restricted."), + name: z.string().describe("The module specifier to restrict."), +}); + +const patternConfigSchema = z.object({ + allowImportNames: z + .array(z.string()) + .optional() + .describe( + "Import names that are explicitly allowed from matching modules.", + ), + allowTypeImports: z + .boolean() + .optional() + .describe("Whether type-only imports from matching modules are allowed."), + group: z + .array(z.string()) + .describe("Glob patterns to match module specifiers against."), + importNames: z + .array(z.string()) + .optional() + .describe("Specific import names to restrict from matching modules."), + message: z + .string() + .optional() + .describe( + "A custom message to display when a matching module is restricted.", + ), +}); + +interface ImportedName { + isTypeOnly: boolean; + name: string; +} + +interface NormalizedPathConfig { + allowImportNames?: string[] | undefined; + allowTypeImports?: boolean | undefined; + importNames?: string[] | undefined; + message?: string | undefined; +} + +interface NormalizedPatternConfig { + allowImportNames?: string[] | undefined; + allowTypeImports?: boolean | undefined; + group: RegExp[]; + importNames?: string[] | undefined; + message?: string | undefined; +} + +function globToRegExp(pattern: string) { + const escaped = pattern + .replace(/[.+^${}()|[\]\\]/g, "\\$&") + .replace(/\*\*/g, "\0") + .replace(/\*/g, "[^/]*") + .replace(/\?/g, "[^/]") + .replace(/\0/g, ".*"); + return new RegExp(`^${escaped}$`); +} + +function hasNameRestrictions(config: { + allowImportNames?: string[] | undefined; + importNames?: string[] | undefined; +}) { + return Boolean(config.importNames ?? config.allowImportNames); +} + +function isNameRestricted( + name: string, + config: { + allowImportNames?: string[] | undefined; + importNames?: string[] | undefined; + }, +) { + if (config.importNames) { + return config.importNames.includes(name); + } + + if (config.allowImportNames) { + return !config.allowImportNames.includes(name); + } + + return true; +} + +export default ruleCreator.createRule(typescriptLanguage, { + about: { + description: "Restricts specified modules from being imported.", + id: "restrictedImports", + }, + messages: { + everythingRestricted: { + primary: + "* import is invalid because '{{ importNames }}' from '{{ source }}' is restricted.", + secondary: [ + "This import uses a namespace or wildcard import, but specific names from this module are restricted.", + "Consider importing only the allowed names explicitly.", + ], + suggestions: [ + "Replace the namespace import with named imports that are allowed.", + ], + }, + everythingRestrictedWithMessage: { + primary: + "* import is invalid because '{{ importNames }}' from '{{ source }}' is restricted. {{ customMessage }}", + secondary: [ + "This import uses a namespace or wildcard import, but specific names from this module are restricted.", + ], + suggestions: [ + "Replace the namespace import with named imports that are allowed.", + ], + }, + importNameRestricted: { + primary: "'{{ importName }}' import from '{{ source }}' is restricted.", + secondary: [ + "This specific import name has been restricted by project configuration.", + "Consider using an alternative API or module.", + ], + suggestions: [ + "Remove this import or replace it with an allowed alternative.", + ], + }, + importNameRestrictedWithMessage: { + primary: + "'{{ importName }}' import from '{{ source }}' is restricted. {{ customMessage }}", + secondary: [ + "This specific import name has been restricted by project configuration.", + ], + suggestions: [ + "Remove this import or replace it with an allowed alternative.", + ], + }, + pathRestricted: { + primary: "'{{ source }}' import is restricted from being used.", + secondary: [ + "This module has been restricted by project configuration.", + "Consider using an alternative module.", + ], + suggestions: [ + "Remove this import or replace it with an allowed alternative.", + ], + }, + pathRestrictedWithMessage: { + primary: + "'{{ source }}' import is restricted from being used. {{ customMessage }}", + secondary: ["This module has been restricted by project configuration."], + suggestions: [ + "Remove this import or replace it with an allowed alternative.", + ], + }, + patternRestricted: { + primary: + "'{{ source }}' import is restricted from being used by a pattern.", + secondary: [ + "This module matches a restricted pattern in the project configuration.", + "Consider using an alternative module.", + ], + suggestions: [ + "Remove this import or replace it with an allowed alternative.", + ], + }, + patternRestrictedWithMessage: { + primary: + "'{{ source }}' import is restricted from being used by a pattern. {{ customMessage }}", + secondary: [ + "This module matches a restricted pattern in the project configuration.", + ], + suggestions: [ + "Remove this import or replace it with an allowed alternative.", + ], + }, + }, + options: { + paths: z + .array(z.union([z.string(), pathConfigSchema])) + .default([]) + .describe("Exact module specifiers to restrict."), + patterns: z + .array(z.union([z.string(), patternConfigSchema])) + .default([]) + .describe("Glob patterns to match restricted module specifiers."), + }, + setup(context) { + const state = { + initialized: false, + normalizedPatterns: [] as NormalizedPatternConfig[], + pathMap: new Map(), + }; + + function ensureInitialized(options: unknown) { + if (state.initialized) { + return; + } + + state.initialized = true; + + const { paths, patterns } = options as { + paths: ( + | string + | { + allowImportNames?: string[] | undefined; + allowTypeImports?: boolean | undefined; + importNames?: string[] | undefined; + message?: string | undefined; + name: string; + } + )[]; + patterns: ( + | string + | { + allowImportNames?: string[] | undefined; + allowTypeImports?: boolean | undefined; + group: string[]; + importNames?: string[] | undefined; + message?: string | undefined; + } + )[]; + }; + + for (const pathEntry of paths) { + if (typeof pathEntry === "string") { + const existing = state.pathMap.get(pathEntry) ?? []; + existing.push({}); + state.pathMap.set(pathEntry, existing); + } else { + const existing = state.pathMap.get(pathEntry.name) ?? []; + existing.push({ + allowImportNames: pathEntry.allowImportNames, + allowTypeImports: pathEntry.allowTypeImports, + importNames: pathEntry.importNames, + message: pathEntry.message, + }); + state.pathMap.set(pathEntry.name, existing); + } + } + + for (const patternEntry of patterns) { + if (typeof patternEntry === "string") { + state.normalizedPatterns.push({ + group: [globToRegExp(patternEntry)], + }); + } else { + state.normalizedPatterns.push({ + allowImportNames: patternEntry.allowImportNames, + allowTypeImports: patternEntry.allowTypeImports, + group: patternEntry.group.map(globToRegExp), + importNames: patternEntry.importNames, + message: patternEntry.message, + }); + } + } + } + + function checkNode( + source: string, + names: ImportedName[], + range: { begin: number; end: number }, + ) { + const pathConfigs = state.pathMap.get(source); + if (pathConfigs) { + for (const config of pathConfigs) { + if (config.allowTypeImports && names.every((n) => n.isTypeOnly)) { + continue; + } + + if (!hasNameRestrictions(config)) { + context.report({ + data: { + customMessage: config.message ?? "", + source, + }, + message: config.message + ? "pathRestrictedWithMessage" + : "pathRestricted", + range, + }); + continue; + } + + for (const imported of names) { + if (config.allowTypeImports && imported.isTypeOnly) { + continue; + } + + if (imported.name === "*") { + context.report({ + data: { + customMessage: config.message ?? "", + importNames: config.importNames?.join("', '") ?? "", + source, + }, + message: config.message + ? "everythingRestrictedWithMessage" + : "everythingRestricted", + range, + }); + continue; + } + + if (isNameRestricted(imported.name, config)) { + context.report({ + data: { + customMessage: config.message ?? "", + importName: imported.name, + source, + }, + message: config.message + ? "importNameRestrictedWithMessage" + : "importNameRestricted", + range, + }); + } + } + } + } + + for (const pattern of state.normalizedPatterns) { + const matches = pattern.group.some((re) => re.test(source)); + if (!matches) { + continue; + } + + if (pattern.allowTypeImports && names.every((n) => n.isTypeOnly)) { + continue; + } + + if (!hasNameRestrictions(pattern)) { + context.report({ + data: { + customMessage: pattern.message ?? "", + source, + }, + message: pattern.message + ? "patternRestrictedWithMessage" + : "patternRestricted", + range, + }); + continue; + } + + for (const imported of names) { + if (pattern.allowTypeImports && imported.isTypeOnly) { + continue; + } + + if (imported.name === "*") { + context.report({ + data: { + customMessage: pattern.message ?? "", + importNames: pattern.importNames?.join("', '") ?? "", + source, + }, + message: pattern.message + ? "everythingRestrictedWithMessage" + : "everythingRestricted", + range, + }); + continue; + } + + if (isNameRestricted(imported.name, pattern)) { + context.report({ + data: { + customMessage: pattern.message ?? "", + importName: imported.name, + source, + }, + message: pattern.message + ? "importNameRestrictedWithMessage" + : "importNameRestricted", + range, + }); + } + } + } + } + + function reportSideEffectRestrictions( + source: string, + range: { begin: number; end: number }, + ) { + const pathConfigs = state.pathMap.get(source); + if (pathConfigs) { + for (const config of pathConfigs) { + if (!hasNameRestrictions(config)) { + context.report({ + data: { + customMessage: config.message ?? "", + source, + }, + message: config.message + ? "pathRestrictedWithMessage" + : "pathRestricted", + range, + }); + } + } + } + + for (const pattern of state.normalizedPatterns) { + const matches = pattern.group.some((re) => re.test(source)); + if (matches && !hasNameRestrictions(pattern)) { + context.report({ + data: { + customMessage: pattern.message ?? "", + source, + }, + message: pattern.message + ? "patternRestrictedWithMessage" + : "patternRestricted", + range, + }); + } + } + } + + return { + visitors: { + ExportDeclaration: (node, { options, sourceFile }) => { + if ( + !node.moduleSpecifier || + !ts.isStringLiteral(node.moduleSpecifier) + ) { + return; + } + + ensureInitialized(options); + + const source = node.moduleSpecifier.text; + const topLevelTypeOnly = node.isTypeOnly; + const range = getTSNodeRange(node, sourceFile); + + let names: ImportedName[]; + if (node.exportClause && ts.isNamedExports(node.exportClause)) { + names = node.exportClause.elements.map((element) => ({ + isTypeOnly: topLevelTypeOnly || element.isTypeOnly, + name: element.propertyName + ? element.propertyName.text + : element.name.text, + })); + } else { + names = [{ isTypeOnly: topLevelTypeOnly, name: "*" }]; + } + + checkNode(source, names, range); + }, + ImportDeclaration: (node, { options, sourceFile }) => { + if (!ts.isStringLiteral(node.moduleSpecifier)) { + return; + } + + ensureInitialized(options); + + const source = node.moduleSpecifier.text; + const range = getTSNodeRange(node, sourceFile); + + // Side-effect import: import "mod" + if (!node.importClause) { + reportSideEffectRestrictions(source, range); + return; + } + + const topLevelTypeOnly = + node.importClause.phaseModifier === SyntaxKind.TypeKeyword; + const names: ImportedName[] = []; + + if (node.importClause.name) { + names.push({ + isTypeOnly: topLevelTypeOnly, + name: "default", + }); + } + + const bindings = node.importClause.namedBindings; + if (bindings) { + if (ts.isNamedImports(bindings)) { + for (const element of bindings.elements) { + names.push({ + isTypeOnly: topLevelTypeOnly || element.isTypeOnly, + name: element.propertyName + ? element.propertyName.text + : element.name.text, + }); + } + } else { + names.push({ + isTypeOnly: topLevelTypeOnly, + name: "*", + }); + } + } + + checkNode(source, names, range); + }, + }, + }; + }, +});