@openrewrite/rewrite 8.68.0-20251124-160231 → 8.68.0-20251125-100455
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/java/tree.d.ts +6 -1
- package/dist/java/tree.d.ts.map +1 -1
- package/dist/java/tree.js +13 -3
- package/dist/java/tree.js.map +1 -1
- package/dist/javascript/add-import.d.ts.map +1 -1
- package/dist/javascript/add-import.js +101 -19
- package/dist/javascript/add-import.js.map +1 -1
- package/dist/javascript/dependency-workspace.d.ts +1 -0
- package/dist/javascript/dependency-workspace.d.ts.map +1 -1
- package/dist/javascript/dependency-workspace.js +44 -0
- package/dist/javascript/dependency-workspace.js.map +1 -1
- package/dist/javascript/templating/pattern.d.ts +7 -0
- package/dist/javascript/templating/pattern.d.ts.map +1 -1
- package/dist/javascript/templating/pattern.js +10 -0
- package/dist/javascript/templating/pattern.js.map +1 -1
- package/dist/javascript/templating/rewrite.d.ts.map +1 -1
- package/dist/javascript/templating/rewrite.js +17 -16
- package/dist/javascript/templating/rewrite.js.map +1 -1
- package/dist/javascript/templating/types.d.ts +56 -28
- package/dist/javascript/templating/types.d.ts.map +1 -1
- package/dist/javascript/type-mapping.d.ts +13 -1
- package/dist/javascript/type-mapping.d.ts.map +1 -1
- package/dist/javascript/type-mapping.js +207 -6
- package/dist/javascript/type-mapping.js.map +1 -1
- package/dist/version.txt +1 -1
- package/package.json +1 -1
- package/src/java/tree.ts +6 -1
- package/src/javascript/add-import.ts +101 -25
- package/src/javascript/dependency-workspace.ts +52 -0
- package/src/javascript/templating/pattern.ts +11 -0
- package/src/javascript/templating/rewrite.ts +19 -18
- package/src/javascript/templating/types.ts +60 -28
- package/src/javascript/type-mapping.ts +230 -7
|
@@ -47,6 +47,42 @@ export class DependencyWorkspace {
|
|
|
47
47
|
// Create/update workspace in target directory
|
|
48
48
|
fs.mkdirSync(targetDir, {recursive: true});
|
|
49
49
|
|
|
50
|
+
// Check if we can reuse a cached workspace by symlinking node_modules
|
|
51
|
+
const hash = this.hashDependencies(dependencies);
|
|
52
|
+
const cachedWorkspaceDir = path.join(this.WORKSPACE_BASE, hash);
|
|
53
|
+
const cachedNodeModules = path.join(cachedWorkspaceDir, 'node_modules');
|
|
54
|
+
|
|
55
|
+
if (fs.existsSync(cachedNodeModules) && this.isWorkspaceValid(cachedWorkspaceDir, dependencies)) {
|
|
56
|
+
// Symlink node_modules from cached workspace
|
|
57
|
+
try {
|
|
58
|
+
const targetNodeModules = path.join(targetDir, 'node_modules');
|
|
59
|
+
|
|
60
|
+
// Remove existing node_modules if present (might be invalid)
|
|
61
|
+
if (fs.existsSync(targetNodeModules)) {
|
|
62
|
+
fs.rmSync(targetNodeModules, {recursive: true, force: true});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Create symlink to cached node_modules
|
|
66
|
+
fs.symlinkSync(cachedNodeModules, targetNodeModules, 'dir');
|
|
67
|
+
|
|
68
|
+
// Write package.json
|
|
69
|
+
const packageJson = {
|
|
70
|
+
name: "openrewrite-template-workspace",
|
|
71
|
+
version: "1.0.0",
|
|
72
|
+
private: true,
|
|
73
|
+
dependencies: dependencies
|
|
74
|
+
};
|
|
75
|
+
fs.writeFileSync(
|
|
76
|
+
path.join(targetDir, 'package.json'),
|
|
77
|
+
JSON.stringify(packageJson, null, 2)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return targetDir;
|
|
81
|
+
} catch (symlinkError) {
|
|
82
|
+
// Symlink failed (e.g., cross-device, permissions) - fall through to npm install
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
50
86
|
try {
|
|
51
87
|
const packageJson = {
|
|
52
88
|
name: "openrewrite-template-workspace",
|
|
@@ -214,6 +250,7 @@ export class DependencyWorkspace {
|
|
|
214
250
|
|
|
215
251
|
/**
|
|
216
252
|
* Checks if a workspace is valid (has node_modules and matching package.json).
|
|
253
|
+
* Handles both real node_modules directories and symlinks to cached workspaces.
|
|
217
254
|
*
|
|
218
255
|
* @param workspaceDir Directory to check
|
|
219
256
|
* @param expectedDependencies Optional dependencies to check against package.json
|
|
@@ -222,10 +259,25 @@ export class DependencyWorkspace {
|
|
|
222
259
|
const nodeModules = path.join(workspaceDir, 'node_modules');
|
|
223
260
|
const packageJsonPath = path.join(workspaceDir, 'package.json');
|
|
224
261
|
|
|
262
|
+
// Check node_modules exists (as directory or symlink)
|
|
225
263
|
if (!fs.existsSync(nodeModules) || !fs.existsSync(packageJsonPath)) {
|
|
226
264
|
return false;
|
|
227
265
|
}
|
|
228
266
|
|
|
267
|
+
// If node_modules is a symlink, verify the target still exists
|
|
268
|
+
try {
|
|
269
|
+
const stats = fs.lstatSync(nodeModules);
|
|
270
|
+
if (stats.isSymbolicLink()) {
|
|
271
|
+
const target = fs.readlinkSync(nodeModules);
|
|
272
|
+
const absoluteTarget = path.isAbsolute(target) ? target : path.resolve(path.dirname(nodeModules), target);
|
|
273
|
+
if (!fs.existsSync(absoluteTarget)) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
229
281
|
// If dependencies provided, check if they match
|
|
230
282
|
if (expectedDependencies) {
|
|
231
283
|
try {
|
|
@@ -616,6 +616,17 @@ export class MatchResult implements IMatchResult {
|
|
|
616
616
|
return this.extractElements(value);
|
|
617
617
|
}
|
|
618
618
|
|
|
619
|
+
/**
|
|
620
|
+
* Checks if a capture has been matched.
|
|
621
|
+
*
|
|
622
|
+
* @param capture The capture name (string) or Capture object
|
|
623
|
+
* @returns true if the capture exists in the match result
|
|
624
|
+
*/
|
|
625
|
+
has(capture: Capture | string): boolean {
|
|
626
|
+
const name = typeof capture === "string" ? capture : ((capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName());
|
|
627
|
+
return this.storage.has(name);
|
|
628
|
+
}
|
|
629
|
+
|
|
619
630
|
/**
|
|
620
631
|
* Extracts semantic elements from storage value.
|
|
621
632
|
* For wrappers, extracts the .element; for arrays, returns array of elements.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import {Cursor, ExecutionContext, Recipe} from '../..';
|
|
17
17
|
import {J} from '../../java';
|
|
18
|
-
import {RewriteRule, RewriteConfig} from './types';
|
|
18
|
+
import {RewriteRule, RewriteConfig, PreMatchContext, PostMatchContext} from './types';
|
|
19
19
|
import {Pattern, MatchResult} from './pattern';
|
|
20
20
|
import {Template} from './template';
|
|
21
21
|
|
|
@@ -26,28 +26,29 @@ class RewriteRuleImpl implements RewriteRule {
|
|
|
26
26
|
constructor(
|
|
27
27
|
private readonly before: Pattern[],
|
|
28
28
|
private readonly after: Template | ((match: MatchResult) => Template),
|
|
29
|
-
private readonly
|
|
30
|
-
private readonly
|
|
29
|
+
private readonly preMatch?: (node: J, context: PreMatchContext) => boolean | Promise<boolean>,
|
|
30
|
+
private readonly postMatch?: (node: J, context: PostMatchContext) => boolean | Promise<boolean>
|
|
31
31
|
) {
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
|
|
35
|
+
// Evaluate preMatch before attempting any pattern matching
|
|
36
|
+
if (this.preMatch) {
|
|
37
|
+
const preMatchResult = await this.preMatch(node, { cursor });
|
|
38
|
+
if (!preMatchResult) {
|
|
39
|
+
return undefined; // Early exit - don't attempt pattern matching
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
for (const pattern of this.before) {
|
|
36
44
|
// Pass cursor to pattern.match() for context-aware capture constraints
|
|
37
45
|
const match = await pattern.match(node, cursor);
|
|
38
46
|
if (match) {
|
|
39
|
-
// Evaluate
|
|
40
|
-
if (this.
|
|
41
|
-
const
|
|
42
|
-
if (!
|
|
43
|
-
continue; // Pattern matched but
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (this.whereNot) {
|
|
48
|
-
const whereNotResult = await this.whereNot(node, cursor);
|
|
49
|
-
if (whereNotResult) {
|
|
50
|
-
continue; // Pattern matched but context is excluded, try next pattern
|
|
47
|
+
// Evaluate postMatch after structural match succeeds
|
|
48
|
+
if (this.postMatch) {
|
|
49
|
+
const postMatchResult = await this.postMatch(node, { cursor, captures: match });
|
|
50
|
+
if (!postMatchResult) {
|
|
51
|
+
continue; // Pattern matched but postMatch failed, try next pattern
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -69,7 +70,7 @@ class RewriteRuleImpl implements RewriteRule {
|
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
// Return undefined if no patterns match or all
|
|
73
|
+
// Return undefined if no patterns match or all postMatch checks failed
|
|
73
74
|
return undefined;
|
|
74
75
|
}
|
|
75
76
|
|
|
@@ -168,8 +169,8 @@ export function rewrite(
|
|
|
168
169
|
return new RewriteRuleImpl(
|
|
169
170
|
Array.isArray(config.before) ? config.before : [config.before],
|
|
170
171
|
config.after,
|
|
171
|
-
config.
|
|
172
|
-
config.
|
|
172
|
+
config.preMatch,
|
|
173
|
+
config.postMatch
|
|
173
174
|
);
|
|
174
175
|
}
|
|
175
176
|
|
|
@@ -590,6 +590,33 @@ export interface RewriteRule {
|
|
|
590
590
|
orElse(alternative: RewriteRule): RewriteRule;
|
|
591
591
|
}
|
|
592
592
|
|
|
593
|
+
/**
|
|
594
|
+
* Context for preMatch predicate - only has cursor, no captures yet.
|
|
595
|
+
*/
|
|
596
|
+
export interface PreMatchContext {
|
|
597
|
+
/**
|
|
598
|
+
* The cursor pointing to the node being considered for matching.
|
|
599
|
+
* Allows navigating the AST (parent, root, etc.).
|
|
600
|
+
*/
|
|
601
|
+
cursor: Cursor;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Context for postMatch predicate - has cursor and captured values.
|
|
606
|
+
*/
|
|
607
|
+
export interface PostMatchContext {
|
|
608
|
+
/**
|
|
609
|
+
* The cursor pointing to the matched node.
|
|
610
|
+
* Allows navigating the AST (parent, root, etc.).
|
|
611
|
+
*/
|
|
612
|
+
cursor: Cursor;
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Values captured during pattern matching.
|
|
616
|
+
*/
|
|
617
|
+
captures: CaptureMap;
|
|
618
|
+
}
|
|
619
|
+
|
|
593
620
|
/**
|
|
594
621
|
* Configuration for a replacement rule.
|
|
595
622
|
*/
|
|
@@ -598,55 +625,52 @@ export interface RewriteConfig {
|
|
|
598
625
|
after: Template | ((match: MatchResult) => Template);
|
|
599
626
|
|
|
600
627
|
/**
|
|
601
|
-
* Optional
|
|
602
|
-
*
|
|
603
|
-
*
|
|
628
|
+
* Optional predicate evaluated BEFORE pattern matching.
|
|
629
|
+
* Use for efficient early filtering based on AST context when captures aren't needed.
|
|
630
|
+
* If this returns false, pattern matching is skipped entirely.
|
|
604
631
|
*
|
|
605
|
-
* @param node The
|
|
606
|
-
* @param
|
|
607
|
-
* @returns true
|
|
632
|
+
* @param node The AST node being considered for matching
|
|
633
|
+
* @param context Context providing cursor for AST navigation
|
|
634
|
+
* @returns true to proceed with pattern matching, false to skip this node
|
|
608
635
|
*
|
|
609
636
|
* @example
|
|
610
637
|
* ```typescript
|
|
611
638
|
* rewrite(() => ({
|
|
612
|
-
* before: pattern`
|
|
613
|
-
* after: template`
|
|
614
|
-
*
|
|
615
|
-
* // Only
|
|
616
|
-
* const method = cursor.firstEnclosing(
|
|
617
|
-
*
|
|
618
|
-
* );
|
|
619
|
-
* return method?.modifiers.some(m => m.type === 'async') || false;
|
|
639
|
+
* before: pattern`console.log(${_('msg')})`,
|
|
640
|
+
* after: template`logger.info(${_('msg')})`,
|
|
641
|
+
* preMatch: (node, {cursor}) => {
|
|
642
|
+
* // Only attempt matching inside functions named 'handleError'
|
|
643
|
+
* const method = cursor.firstEnclosing(isMethodDeclaration);
|
|
644
|
+
* return method?.name.simpleName === 'handleError';
|
|
620
645
|
* }
|
|
621
646
|
* }));
|
|
622
647
|
* ```
|
|
623
648
|
*/
|
|
624
|
-
|
|
649
|
+
preMatch?: (node: J, context: PreMatchContext) => boolean | Promise<boolean>;
|
|
625
650
|
|
|
626
651
|
/**
|
|
627
|
-
* Optional
|
|
628
|
-
*
|
|
629
|
-
*
|
|
652
|
+
* Optional predicate evaluated AFTER pattern matching succeeds.
|
|
653
|
+
* Use when you need access to captured values to decide whether to apply the transformation.
|
|
654
|
+
* If this returns false, the transformation is not applied.
|
|
630
655
|
*
|
|
631
656
|
* @param node The matched AST node
|
|
632
|
-
* @param
|
|
633
|
-
* @returns true
|
|
657
|
+
* @param context Context providing cursor for AST navigation and captured values
|
|
658
|
+
* @returns true to apply the transformation, false to skip
|
|
634
659
|
*
|
|
635
660
|
* @example
|
|
636
661
|
* ```typescript
|
|
637
662
|
* rewrite(() => ({
|
|
638
|
-
* before: pattern
|
|
639
|
-
* after: template
|
|
640
|
-
*
|
|
641
|
-
* //
|
|
642
|
-
*
|
|
643
|
-
*
|
|
644
|
-
* ) !== undefined;
|
|
663
|
+
* before: pattern`${_('a')} + ${_('b')}`,
|
|
664
|
+
* after: template`${_('b')} + ${_('a')}`,
|
|
665
|
+
* postMatch: (node, {cursor, captures}) => {
|
|
666
|
+
* // Only swap if 'a' is a literal number
|
|
667
|
+
* const a = captures.get('a');
|
|
668
|
+
* return a?.kind === J.Kind.Literal && typeof a.value === 'number';
|
|
645
669
|
* }
|
|
646
670
|
* }));
|
|
647
671
|
* ```
|
|
648
672
|
*/
|
|
649
|
-
|
|
673
|
+
postMatch?: (node: J, context: PostMatchContext) => boolean | Promise<boolean>;
|
|
650
674
|
}
|
|
651
675
|
|
|
652
676
|
/**
|
|
@@ -755,6 +779,14 @@ export interface MatchResult {
|
|
|
755
779
|
get(capture: string): any;
|
|
756
780
|
|
|
757
781
|
get<T>(capture: Capture<T>): T | undefined;
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Checks if a capture has been matched.
|
|
785
|
+
*
|
|
786
|
+
* @param capture The capture name (string) or Capture object
|
|
787
|
+
* @returns true if the capture exists in the match result
|
|
788
|
+
*/
|
|
789
|
+
has(capture: Capture | string): boolean;
|
|
758
790
|
}
|
|
759
791
|
|
|
760
792
|
/**
|
|
@@ -65,6 +65,16 @@ export class JavaScriptTypeMapping {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
type(node: ts.Node): Type | undefined {
|
|
68
|
+
// For identifiers, check if this references a variable
|
|
69
|
+
// This enables fieldType attribution for variable references
|
|
70
|
+
if (ts.isIdentifier(node)) {
|
|
71
|
+
const variableType = this.variableType(node);
|
|
72
|
+
if (variableType) {
|
|
73
|
+
return variableType;
|
|
74
|
+
}
|
|
75
|
+
// Fall through to regular type checking if not a variable
|
|
76
|
+
}
|
|
77
|
+
|
|
68
78
|
let type: ts.Type | undefined;
|
|
69
79
|
if (ts.isExpression(node)) {
|
|
70
80
|
type = this.checker.getTypeAtLocation(node);
|
|
@@ -318,16 +328,229 @@ export class JavaScriptTypeMapping {
|
|
|
318
328
|
return Type.isPrimitive(type) ? type : Type.Primitive.None;
|
|
319
329
|
}
|
|
320
330
|
|
|
321
|
-
variableType(node: ts.
|
|
331
|
+
variableType(node: ts.Node): Type.Variable | undefined {
|
|
332
|
+
let symbol: ts.Symbol | undefined;
|
|
333
|
+
let location: ts.Node = node;
|
|
334
|
+
|
|
335
|
+
// Get the symbol depending on node type
|
|
322
336
|
if (ts.isVariableDeclaration(node)) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
337
|
+
symbol = this.checker.getSymbolAtLocation(node.name);
|
|
338
|
+
} else if (ts.isParameter(node)) {
|
|
339
|
+
symbol = this.checker.getSymbolAtLocation(node.name);
|
|
340
|
+
} else if (ts.isIdentifier(node)) {
|
|
341
|
+
// For identifier references (like 'vi' in 'vi.fn()')
|
|
342
|
+
symbol = this.checker.getSymbolAtLocation(node);
|
|
343
|
+
} else if (ts.isPropertyDeclaration(node) || ts.isPropertySignature(node)) {
|
|
344
|
+
symbol = this.checker.getSymbolAtLocation(node.name);
|
|
345
|
+
} else {
|
|
346
|
+
// Not a variable/parameter/property we can handle
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!symbol) {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Get the variable declaration (resolve aliases if needed)
|
|
355
|
+
let actualSymbol = symbol;
|
|
356
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
357
|
+
actualSymbol = this.checker.getAliasedSymbol(symbol);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Check if this symbol represents a variable, parameter, or property
|
|
361
|
+
// Exclude functions, classes, interfaces, namespaces, type aliases
|
|
362
|
+
const isExcluded = actualSymbol.flags & (
|
|
363
|
+
ts.SymbolFlags.Function |
|
|
364
|
+
ts.SymbolFlags.Class |
|
|
365
|
+
ts.SymbolFlags.Interface |
|
|
366
|
+
ts.SymbolFlags.Enum |
|
|
367
|
+
ts.SymbolFlags.ValueModule |
|
|
368
|
+
ts.SymbolFlags.NamespaceModule |
|
|
369
|
+
ts.SymbolFlags.TypeAlias |
|
|
370
|
+
ts.SymbolFlags.TypeParameter
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
if (isExcluded) {
|
|
374
|
+
// Not a variable - it's a type, function, class, namespace, etc.
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const isVariable = actualSymbol.flags & (
|
|
379
|
+
ts.SymbolFlags.Variable |
|
|
380
|
+
ts.SymbolFlags.Property |
|
|
381
|
+
ts.SymbolFlags.FunctionScopedVariable |
|
|
382
|
+
ts.SymbolFlags.BlockScopedVariable
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
if (!isVariable) {
|
|
386
|
+
// Not a variable we recognize
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Get the type of the variable
|
|
391
|
+
const variableType = this.checker.getTypeOfSymbolAtLocation(actualSymbol, location);
|
|
392
|
+
const mappedType = this.getType(variableType);
|
|
393
|
+
|
|
394
|
+
// Get the owner (declaring type) for the variable
|
|
395
|
+
let ownerType: Type | undefined;
|
|
396
|
+
|
|
397
|
+
// Check if the variable is imported
|
|
398
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
399
|
+
// For imported variables, find the module specifier
|
|
400
|
+
const declarations = symbol.declarations;
|
|
401
|
+
if (declarations && declarations.length > 0) {
|
|
402
|
+
let importNode: ts.Node | undefined = declarations[0];
|
|
403
|
+
|
|
404
|
+
// Traverse up to find the ImportDeclaration
|
|
405
|
+
while (importNode && !ts.isImportDeclaration(importNode)) {
|
|
406
|
+
importNode = importNode.parent;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (importNode && ts.isImportDeclaration(importNode)) {
|
|
410
|
+
const importDecl = importNode as ts.ImportDeclaration;
|
|
411
|
+
if (ts.isStringLiteral(importDecl.moduleSpecifier)) {
|
|
412
|
+
const moduleSpecifier = importDecl.moduleSpecifier.text;
|
|
413
|
+
// Create a Type.Class representing the module
|
|
414
|
+
ownerType = Object.assign(new NonDraftableType(), {
|
|
415
|
+
kind: Type.Kind.Class,
|
|
416
|
+
flags: 0,
|
|
417
|
+
classKind: Type.Class.Kind.Interface,
|
|
418
|
+
fullyQualifiedName: moduleSpecifier,
|
|
419
|
+
typeParameters: [],
|
|
420
|
+
annotations: [],
|
|
421
|
+
interfaces: [],
|
|
422
|
+
members: [],
|
|
423
|
+
methods: [],
|
|
424
|
+
toJSON: function () {
|
|
425
|
+
return Type.signature(this);
|
|
426
|
+
}
|
|
427
|
+
}) as Type.Class;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
328
430
|
}
|
|
431
|
+
} else {
|
|
432
|
+
// For non-imported variables, check if they belong to a class/interface/namespace
|
|
433
|
+
const parentSymbol = (actualSymbol as any).parent as ts.Symbol | undefined;
|
|
434
|
+
if (parentSymbol) {
|
|
435
|
+
const parentType = this.checker.getDeclaredTypeOfSymbol(parentSymbol);
|
|
436
|
+
if (parentType) {
|
|
437
|
+
ownerType = this.getType(parentType);
|
|
438
|
+
|
|
439
|
+
// If the parent is a namespace, try to find the module it came from
|
|
440
|
+
// This handles cases like React.forwardRef where the namespace is React
|
|
441
|
+
// but the module is "react"
|
|
442
|
+
if (parentSymbol.flags & ts.SymbolFlags.ValueModule ||
|
|
443
|
+
parentSymbol.flags & ts.SymbolFlags.NamespaceModule) {
|
|
444
|
+
// Check if this namespace was imported
|
|
445
|
+
const parentDeclarations = parentSymbol.declarations;
|
|
446
|
+
if (parentDeclarations && parentDeclarations.length > 0) {
|
|
447
|
+
const firstDecl = parentDeclarations[0];
|
|
448
|
+
const sourceFile = firstDecl.getSourceFile();
|
|
449
|
+
// If it's from node_modules or a .d.ts file, try to extract the module name
|
|
450
|
+
if (sourceFile.isDeclarationFile) {
|
|
451
|
+
const fileName = sourceFile.fileName;
|
|
452
|
+
const moduleName = this.extractModuleNameFromPath(fileName);
|
|
453
|
+
if (moduleName) {
|
|
454
|
+
// Store the module as the owningClass for now
|
|
455
|
+
// (This is a bit of a hack, but works with the current type system)
|
|
456
|
+
if (Type.isClass(ownerType)) {
|
|
457
|
+
(ownerType as any).owningClass = Object.assign(new NonDraftableType(), {
|
|
458
|
+
kind: Type.Kind.Class,
|
|
459
|
+
flags: 0,
|
|
460
|
+
classKind: Type.Class.Kind.Interface,
|
|
461
|
+
fullyQualifiedName: moduleName,
|
|
462
|
+
typeParameters: [],
|
|
463
|
+
annotations: [],
|
|
464
|
+
interfaces: [],
|
|
465
|
+
members: [],
|
|
466
|
+
methods: [],
|
|
467
|
+
toJSON: function () {
|
|
468
|
+
return Type.signature(this);
|
|
469
|
+
}
|
|
470
|
+
}) as Type.Class;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Create the Type.Variable
|
|
481
|
+
const variable = Object.assign(new NonDraftableType(), {
|
|
482
|
+
kind: Type.Kind.Variable,
|
|
483
|
+
name: actualSymbol.getName(),
|
|
484
|
+
owner: ownerType,
|
|
485
|
+
type: mappedType,
|
|
486
|
+
annotations: [],
|
|
487
|
+
toJSON: function () {
|
|
488
|
+
return Type.signature(this);
|
|
489
|
+
}
|
|
490
|
+
}) as Type.Variable;
|
|
491
|
+
|
|
492
|
+
return variable;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Extract the npm module name from a file path.
|
|
497
|
+
* Handles various package manager layouts:
|
|
498
|
+
* - Standard: /path/node_modules/react/index.d.ts -> react
|
|
499
|
+
* - Scoped: /path/node_modules/@types/react/index.d.ts -> react
|
|
500
|
+
* - Scoped with __ encoding: /path/node_modules/@types/testing-library__react/index.d.ts -> @testing-library/react
|
|
501
|
+
* - Nested node_modules: /path/node_modules/pkg/node_modules/dep/index.d.ts -> dep
|
|
502
|
+
* - pnpm: /path/node_modules/.pnpm/react@18.2.0/node_modules/react/index.d.ts -> react
|
|
503
|
+
*
|
|
504
|
+
* @returns The module name, or undefined if not from node_modules
|
|
505
|
+
*/
|
|
506
|
+
private extractModuleNameFromPath(fileName: string): string | undefined {
|
|
507
|
+
if (!fileName.includes('node_modules/')) {
|
|
508
|
+
return undefined;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Find the last occurrence of node_modules/ to handle nested dependencies
|
|
512
|
+
// This also correctly handles pnpm's .pnpm structure
|
|
513
|
+
const lastNodeModulesIndex = fileName.lastIndexOf('node_modules/');
|
|
514
|
+
const afterNodeModules = fileName.substring(lastNodeModulesIndex + 'node_modules/'.length);
|
|
515
|
+
|
|
516
|
+
// Split by '/' to get path segments
|
|
517
|
+
const segments = afterNodeModules.split('/');
|
|
518
|
+
if (segments.length === 0) {
|
|
519
|
+
return undefined;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
let moduleName: string;
|
|
523
|
+
|
|
524
|
+
// Handle scoped packages (@scope/package)
|
|
525
|
+
if (segments[0].startsWith('@') && segments.length > 1) {
|
|
526
|
+
moduleName = `${segments[0]}/${segments[1]}`;
|
|
527
|
+
} else {
|
|
528
|
+
moduleName = segments[0];
|
|
329
529
|
}
|
|
330
|
-
|
|
530
|
+
|
|
531
|
+
// Skip pnpm's .pnpm directory - it contains versioned package paths
|
|
532
|
+
// In pnpm, the actual package is in: .pnpm/pkg@version/node_modules/pkg
|
|
533
|
+
// So we already handled this by using lastIndexOf above
|
|
534
|
+
if (moduleName === '.pnpm') {
|
|
535
|
+
return undefined;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Remove @types/ prefix and decode DefinitelyTyped scoped package encoding
|
|
539
|
+
// DefinitelyTyped encodes scoped packages using __ instead of /
|
|
540
|
+
// Example: @types/testing-library__react -> @testing-library/react
|
|
541
|
+
if (moduleName.startsWith('@types/')) {
|
|
542
|
+
moduleName = moduleName.substring('@types/'.length);
|
|
543
|
+
// Decode __ encoding for scoped packages
|
|
544
|
+
// testing-library__react -> @testing-library/react
|
|
545
|
+
if (moduleName.includes('__')) {
|
|
546
|
+
const parts = moduleName.split('__');
|
|
547
|
+
if (parts.length === 2) {
|
|
548
|
+
moduleName = `@${parts[0]}/${parts[1]}`;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return moduleName;
|
|
331
554
|
}
|
|
332
555
|
|
|
333
556
|
/**
|