1212 * insertion safely — only one survives.
1313 */
1414
15+ /**
16+ * Build the fixer-side inserts for missing import + optional hoist.
17+ * Returns an array of fixer operations the caller appends to its own
18+ * fix() return value.
19+ *
20+ * summary — output of summarizeImportTarget()
21+ * fixer — the fixer passed to context.report({ fix })
22+ * importLine — the literal `import { ... } from '...'` text
23+ * hoistLine — optional; the literal `const x = ...()` text
24+ */
25+ export function appendImportFixes ( summary , fixer , importLine , hoistLine ) {
26+ const ops = [ ]
27+ if ( ! summary . hasImport ) {
28+ if ( summary . lastImport ) {
29+ ops . push ( fixer . insertTextAfter ( summary . lastImport , `\n${ importLine } ` ) )
30+ } else {
31+ ops . push ( fixer . insertTextBeforeRange ( [ 0 , 0 ] , `${ importLine } \n` ) )
32+ }
33+ }
34+ if ( hoistLine && ! summary . hasLocal ) {
35+ if ( summary . lastImport ) {
36+ ops . push ( fixer . insertTextAfter ( summary . lastImport , `\n\n${ hoistLine } ` ) )
37+ } else {
38+ ops . push ( fixer . insertTextBeforeRange ( [ 0 , 0 ] , `${ hoistLine } \n\n` ) )
39+ }
40+ }
41+ return ops
42+ }
43+
1544/**
1645 * Walk a Program node body once and figure out:
1746 * - the last top-level ImportDeclaration node (or undefined)
18- * - whether `importName` is already imported from `specifier`
47+ * - whether `importName` is already imported ( from ANY source)
1948 * - whether a top-level `localName` identifier already exists
2049 * (any const/let/var or import-as-local with that name)
50+ *
51+ * Import detection ignores the specifier path: a file inside the lib
52+ * package itself imports `getDefaultLogger` from `'../logger'`, while
53+ * a downstream repo imports the same name from
54+ * `'@socketsecurity/lib/logger'`. Both resolve to the same identifier;
55+ * either should count as "already imported" so the autofix doesn't
56+ * inject a duplicate (and broken — see issue #64).
57+ *
58+ * `specifier` is retained in the signature for backward compatibility
59+ * but is no longer used for the match decision. Callers may pass any
60+ * truthy value (typically the canonical package path the rule would
61+ * inject if the import were missing).
2162 */
22- export function summarizeImportTarget ( program , specifier , importName , localName ) {
63+ export function summarizeImportTarget (
64+ program ,
65+ // eslint-disable-next-line no-unused-vars
66+ specifier ,
67+ importName ,
68+ localName ,
69+ ) {
2370 let lastImport
2471 let hasImport = false
2572 let hasLocal = false
2673 for ( const stmt of program . body ) {
2774 if ( stmt . type === 'ImportDeclaration' ) {
2875 lastImport = stmt
29- const source = stmt . source && stmt . source . value
30- if ( source === specifier ) {
31- for ( const spec of stmt . specifiers ) {
32- if (
33- spec . type === 'ImportSpecifier' &&
34- spec . imported &&
35- spec . imported . name === importName
36- ) {
37- hasImport = true
38- }
39- if ( localName && spec . local && spec . local . name === localName ) {
40- hasLocal = true
41- }
76+ for ( const spec of stmt . specifiers ) {
77+ if (
78+ spec . type === 'ImportSpecifier' &&
79+ spec . imported &&
80+ spec . imported . name === importName
81+ ) {
82+ hasImport = true
83+ }
84+ if (
85+ localName &&
86+ spec . local &&
87+ spec . local . name === localName &&
88+ ( spec . type === 'ImportSpecifier' ||
89+ spec . type === 'ImportDefaultSpecifier' ||
90+ spec . type === 'ImportNamespaceSpecifier' )
91+ ) {
92+ hasLocal = true
4293 }
4394 }
4495 continue
@@ -57,32 +108,3 @@ export function summarizeImportTarget(program, specifier, importName, localName)
57108 }
58109 return { hasImport, hasLocal, lastImport }
59110}
60-
61- /**
62- * Build the fixer-side inserts for missing import + optional hoist.
63- * Returns an array of fixer operations the caller appends to its own
64- * fix() return value.
65- *
66- * summary — output of summarizeImportTarget()
67- * fixer — the fixer passed to context.report({ fix })
68- * importLine — the literal `import { ... } from '...'` text
69- * hoistLine — optional; the literal `const x = ...()` text
70- */
71- export function appendImportFixes ( summary , fixer , importLine , hoistLine ) {
72- const ops = [ ]
73- if ( ! summary . hasImport ) {
74- if ( summary . lastImport ) {
75- ops . push ( fixer . insertTextAfter ( summary . lastImport , `\n${ importLine } ` ) )
76- } else {
77- ops . push ( fixer . insertTextBeforeRange ( [ 0 , 0 ] , `${ importLine } \n` ) )
78- }
79- }
80- if ( hoistLine && ! summary . hasLocal ) {
81- if ( summary . lastImport ) {
82- ops . push ( fixer . insertTextAfter ( summary . lastImport , `\n\n${ hoistLine } ` ) )
83- } else {
84- ops . push ( fixer . insertTextBeforeRange ( [ 0 , 0 ] , `${ hoistLine } \n\n` ) )
85- }
86- }
87- return ops
88- }
0 commit comments