Skip to content

Commit e85cc89

Browse files
committed
Add support for namespace, mixed, and TypeScript type imports
Extends import detection to handle all valid JavaScript/TypeScript patterns: - Namespace imports: import * as Pattern from "package" - Mixed default + named: import React, { useState } from "react" - Mixed default + namespace: import React, * as All from "react" - TypeScript type imports: import type { Card } from "package" Updated regex pattern with 5 capture groups: 1. Default import (when mixed with named/namespace) 2. Named imports (braced list) 3. Default import (when standalone) 4. Namespace import 5. Package name The regex now includes optional "type" keyword support for TypeScript type-only imports, ensuring comprehensive coverage of modern JavaScript and TypeScript import patterns. Pattern matching logic updated to: - Check all three import types (default, named, namespace) - Use exact match for single identifiers (default, namespace) - Use substring match for comma-separated lists (named imports) - Added inline comments explaining matching strategy Test coverage (25 test cases): - Namespace imports (single line, multiline, multiple per file) - Mixed default + named imports (both parts searchable, multiline) - Mixed default + namespace imports (rare but valid) - TypeScript type imports (named, default, namespace, multiline) - Side-effect imports (correctly ignored) - Word boundaries (avoid partial matches like "Card" vs "CardHelper") - Special characters in package names (@scoped/packages) - Stress test with 20 named imports in single statement - Pattern not found scenarios All 25 tests passing. Build successful. Addresses code review feedback item #1. Signed-off-by: tsanders <[email protected]>
1 parent c777cfb commit e85cc89

File tree

2 files changed

+219
-14
lines changed

2 files changed

+219
-14
lines changed

external-providers/generic-external-provider/pkg/server_configurations/nodejs/import_search_test.go

Lines changed: 179 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,13 @@ export const MyCard = Card;`,
136136
expectedColumn: 9,
137137
},
138138
{
139-
name: "Type import (not currently supported, but shouldn't crash)",
139+
name: "TypeScript type import - named",
140140
fileContent: `import type { Button } from '@patternfly/react-core';
141141
export const MyButton = Button;`,
142-
pattern: "Button",
143-
expectedCount: 0, // Not matched by current regex
142+
pattern: "Button",
143+
expectedCount: 1,
144+
expectedLine: 0,
145+
expectedColumn: 14,
144146
},
145147
{
146148
name: "Mixed imports",
@@ -152,6 +154,180 @@ import { useState } from 'react';`,
152154
expectedLine: 1,
153155
expectedColumn: 9,
154156
},
157+
{
158+
name: "Namespace import",
159+
fileContent: `import * as PatternFly from '@patternfly/react-core';
160+
export const MyCard = PatternFly.Card;`,
161+
pattern: "PatternFly",
162+
expectedCount: 1,
163+
expectedLine: 0,
164+
expectedColumn: 12,
165+
},
166+
{
167+
name: "Namespace import - multiline",
168+
fileContent: `import * as
169+
PatternFly
170+
from '@patternfly/react-core';`,
171+
pattern: "PatternFly",
172+
expectedCount: 1,
173+
expectedLine: 1,
174+
expectedColumn: 0,
175+
},
176+
{
177+
name: "Default + named import (mixed)",
178+
fileContent: `import React, { useState, useEffect } from 'react';
179+
export const Component = () => {};`,
180+
pattern: "React",
181+
expectedCount: 1,
182+
expectedLine: 0,
183+
expectedColumn: 7,
184+
},
185+
{
186+
name: "Default + named import - search named",
187+
fileContent: `import React, { useState, useEffect } from 'react';
188+
export const Component = () => {};`,
189+
pattern: "useState",
190+
expectedCount: 1,
191+
expectedLine: 0,
192+
expectedColumn: 16,
193+
},
194+
{
195+
name: "Default + namespace import (rare)",
196+
fileContent: `import React, * as ReactAll from 'react';
197+
export const Component = () => {};`,
198+
pattern: "ReactAll",
199+
expectedCount: 1,
200+
expectedLine: 0,
201+
expectedColumn: 19,
202+
},
203+
{
204+
name: "Side-effect import - no symbols",
205+
fileContent: `import '@patternfly/react-core/dist/styles/base.css';
206+
export const Component = () => {};`,
207+
pattern: "Card",
208+
expectedCount: 0,
209+
},
210+
{
211+
name: "Namespace import - pattern not found",
212+
fileContent: `import * as PatternFly from '@patternfly/react-core';
213+
export const MyCard = PatternFly.Card;`,
214+
pattern: "Card",
215+
expectedCount: 0, // "Card" is not the namespace identifier
216+
},
217+
{
218+
name: "Default + named - search default part",
219+
fileContent: `import React, { useState } from 'react';
220+
const x = useState();`,
221+
pattern: "React",
222+
expectedCount: 1,
223+
expectedLine: 0,
224+
expectedColumn: 7,
225+
},
226+
{
227+
name: "Default + namespace - search default part",
228+
fileContent: `import React, * as ReactAll from 'react';
229+
const x = ReactAll.useState();`,
230+
pattern: "React",
231+
expectedCount: 1,
232+
expectedLine: 0,
233+
expectedColumn: 7,
234+
},
235+
{
236+
name: "Multiline default + named",
237+
fileContent: `import React, {
238+
useState,
239+
useEffect
240+
} from 'react';`,
241+
pattern: "useEffect",
242+
expectedCount: 1,
243+
expectedLine: 2,
244+
expectedColumn: 2,
245+
},
246+
{
247+
name: "Multiple namespace imports in file",
248+
fileContent: `import * as PF from '@patternfly/react-core';
249+
import * as Icons from '@patternfly/react-icons';
250+
import * as Hooks from '@patternfly/react-hooks';`,
251+
pattern: "Icons",
252+
expectedCount: 1,
253+
expectedLine: 1,
254+
expectedColumn: 12,
255+
},
256+
{
257+
name: "Namespace with special chars in package name",
258+
fileContent: `import * as Util from '@company/util-package';
259+
export const test = Util.helper();`,
260+
pattern: "Util",
261+
expectedCount: 1,
262+
expectedLine: 0,
263+
expectedColumn: 12,
264+
},
265+
{
266+
name: "Word boundary in namespace import",
267+
fileContent: `import * as Card from '@patternfly/react-core';
268+
import * as CardHelper from './helpers';`,
269+
pattern: "Card",
270+
expectedCount: 1,
271+
expectedLine: 0,
272+
expectedColumn: 12, // Should only match first, not CardHelper
273+
},
274+
{
275+
name: "Stress test - very long multiline import",
276+
fileContent: `import {
277+
Button,
278+
Card,
279+
CardBody,
280+
CardHeader,
281+
CardTitle,
282+
Chip,
283+
ChipGroup,
284+
Label,
285+
Badge,
286+
Alert,
287+
AlertGroup,
288+
Modal,
289+
ModalVariant,
290+
Toolbar,
291+
ToolbarContent,
292+
ToolbarItem,
293+
Select,
294+
SelectOption,
295+
SelectVariant
296+
} from '@patternfly/react-core';`,
297+
pattern: "ModalVariant",
298+
expectedCount: 1,
299+
expectedLine: 13,
300+
expectedColumn: 2,
301+
},
302+
{
303+
name: "TypeScript type import - default",
304+
fileContent: `import type React from 'react';
305+
type Props = { children: React.ReactNode };`,
306+
pattern: "React",
307+
expectedCount: 1,
308+
expectedLine: 0,
309+
expectedColumn: 12,
310+
},
311+
{
312+
name: "TypeScript type import - namespace",
313+
fileContent: `import type * as Types from './types';
314+
type MyType = Types.User;`,
315+
pattern: "Types",
316+
expectedCount: 1,
317+
expectedLine: 0,
318+
expectedColumn: 17,
319+
},
320+
{
321+
name: "TypeScript type import - multiline",
322+
fileContent: `import type {
323+
ButtonProps,
324+
CardProps
325+
} from '@patternfly/react-core';`,
326+
pattern: "CardProps",
327+
expectedCount: 1,
328+
expectedLine: 2,
329+
expectedColumn: 2,
330+
},
155331
}
156332

157333
for _, tc := range testCases {

external-providers/generic-external-provider/pkg/server_configurations/nodejs/service_client.go

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -505,10 +505,25 @@ func (sc *NodeServiceClient) EvaluateSymbols(ctx context.Context, symbols []prot
505505
// The returned locations can be used with LSP textDocument/definition to find where
506506
// the symbol is actually defined, enabling efficient reference lookup.
507507
func (sc *NodeServiceClient) findImportStatements(pattern string, files []fileInfo) []ImportLocation {
508-
// Regex to match: import { Pattern, ... } from "package"
509-
// Captures both named imports and default imports
508+
// Regex to match all valid import statement patterns:
509+
// - import { Card } from "pkg" (named only)
510+
// - import Card from "pkg" (default only)
511+
// - import * as Card from "pkg" (namespace only)
512+
// - import React, { useState } from "pkg" (default + named)
513+
// - import React, * as All from "pkg" (default + namespace - rare but valid)
514+
// - import type { Card } from "pkg" (TypeScript type-only import)
515+
// - import type Card from "pkg" (TypeScript type-only default import)
516+
//
517+
// The optional "type" keyword is supported but ignored (TypeScript type-only imports).
518+
//
519+
// Capture groups:
520+
// 1: Default import (when mixed with named/namespace)
521+
// 2: Named imports (braced list)
522+
// 3: Default import (when standalone)
523+
// 4: Namespace import (standalone or after default)
524+
// 5: Package name
510525
importRegex := regexp.MustCompile(
511-
`import\s+(?:\{([^}]*)\}|(\w+))\s+from\s+['"]([^'"]+)['"]`,
526+
`import\s+(?:type\s+)?(?:(\w+)\s*,\s*)?(?:\{([^}]*)\}|(\w+)|\*\s+as\s+(\w+))\s+from\s+['"]([^'"]+)['"]`,
512527
)
513528

514529
var locations []ImportLocation
@@ -533,24 +548,38 @@ func (sc *NodeServiceClient) findImportStatements(pattern string, files []fileIn
533548
continue
534549
}
535550

536-
var namedImports string
537551
var defaultImport string
552+
var namedImports string
553+
var namespaceImport string
538554

539-
// Extract named imports (group 1)
555+
// Extract default import (group 1 for mixed, group 3 for standalone)
540556
if matchIdx[2] != -1 && matchIdx[3] != -1 {
541-
namedImports = normalized[matchIdx[2]:matchIdx[3]]
557+
// Default import in mixed form: import React, { ... }
558+
defaultImport = normalized[matchIdx[2]:matchIdx[3]]
559+
} else if matchIdx[6] != -1 && matchIdx[7] != -1 {
560+
// Default import standalone: import React from "..."
561+
defaultImport = normalized[matchIdx[6]:matchIdx[7]]
542562
}
543563

544-
// Extract default import (group 2)
564+
// Extract named imports (group 2)
545565
if matchIdx[4] != -1 && matchIdx[5] != -1 {
546-
defaultImport = normalized[matchIdx[4]:matchIdx[5]]
566+
namedImports = normalized[matchIdx[4]:matchIdx[5]]
547567
}
548568

549-
// Check if pattern appears in named imports or is the default import
569+
// Extract namespace import (group 4)
570+
if matchIdx[8] != -1 && matchIdx[9] != -1 {
571+
namespaceImport = normalized[matchIdx[8]:matchIdx[9]]
572+
}
573+
574+
// Check if pattern matches any import type
575+
// - Default and namespace imports use exact match (single identifier)
576+
// - Named imports use substring match (comma-separated list may contain the pattern)
550577
patternFound := false
551-
if namedImports != "" && strings.Contains(namedImports, pattern) {
578+
if defaultImport == pattern {
579+
patternFound = true
580+
} else if namedImports != "" && strings.Contains(namedImports, pattern) {
552581
patternFound = true
553-
} else if defaultImport != "" && defaultImport == pattern {
582+
} else if namespaceImport == pattern {
554583
patternFound = true
555584
}
556585

0 commit comments

Comments
 (0)