@openrewrite/rewrite 8.67.0 → 8.67.1
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/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/java/tree.d.ts +7 -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 +200 -44
- package/dist/javascript/add-import.js.map +1 -1
- package/dist/javascript/cleanup/index.d.ts +2 -0
- package/dist/javascript/cleanup/index.d.ts.map +1 -0
- package/dist/javascript/cleanup/index.js +21 -0
- package/dist/javascript/cleanup/index.js.map +1 -0
- package/dist/javascript/cleanup/use-object-property-shorthand.d.ts +22 -0
- package/dist/javascript/cleanup/use-object-property-shorthand.d.ts.map +1 -0
- package/dist/javascript/cleanup/use-object-property-shorthand.js +144 -0
- package/dist/javascript/cleanup/use-object-property-shorthand.js.map +1 -0
- package/dist/javascript/comparator.d.ts +8 -0
- package/dist/javascript/comparator.d.ts.map +1 -1
- package/dist/javascript/comparator.js +12 -0
- package/dist/javascript/comparator.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/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +21 -1
- package/dist/javascript/parser.js.map +1 -1
- package/dist/javascript/print.d.ts +1 -0
- package/dist/javascript/print.d.ts.map +1 -1
- package/dist/javascript/print.js +8 -0
- package/dist/javascript/print.js.map +1 -1
- package/dist/javascript/rpc.js +14 -0
- package/dist/javascript/rpc.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/tree.d.ts +9 -0
- package/dist/javascript/tree.d.ts.map +1 -1
- package/dist/javascript/tree.js +1 -0
- package/dist/javascript/tree.js.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 +215 -8
- package/dist/javascript/type-mapping.js.map +1 -1
- package/dist/javascript/visitor.d.ts +1 -0
- package/dist/javascript/visitor.d.ts.map +1 -1
- package/dist/javascript/visitor.js +11 -0
- package/dist/javascript/visitor.js.map +1 -1
- package/dist/json/parser.js +18 -2
- package/dist/json/parser.js.map +1 -1
- package/dist/version.txt +1 -1
- package/package.json +1 -1
- package/src/index.ts +3 -0
- package/src/java/tree.ts +7 -1
- package/src/javascript/add-import.ts +211 -53
- package/src/javascript/cleanup/index.ts +17 -0
- package/src/javascript/cleanup/use-object-property-shorthand.ts +154 -0
- package/src/javascript/comparator.ts +11 -0
- package/src/javascript/dependency-workspace.ts +52 -0
- package/src/javascript/parser.ts +23 -1
- package/src/javascript/print.ts +7 -0
- package/src/javascript/rpc.ts +12 -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/tree.ts +10 -0
- package/src/javascript/type-mapping.ts +239 -9
- package/src/javascript/visitor.ts +10 -0
- package/src/json/parser.ts +16 -2
package/src/javascript/parser.ts
CHANGED
|
@@ -366,6 +366,26 @@ export class JavaScriptParserVisitor {
|
|
|
366
366
|
}
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
let shebangStatement: J.RightPadded<JS.Shebang> | undefined;
|
|
370
|
+
if (prefix.whitespace?.startsWith('#!')) {
|
|
371
|
+
const newlineIndex = prefix.whitespace.indexOf('\n');
|
|
372
|
+
const shebangText = newlineIndex === -1 ? prefix.whitespace : prefix.whitespace.slice(0, newlineIndex);
|
|
373
|
+
const afterShebang = newlineIndex === -1 ? '' : '\n';
|
|
374
|
+
const remainingWhitespace = newlineIndex === -1 ? '' : prefix.whitespace.slice(newlineIndex + 1);
|
|
375
|
+
|
|
376
|
+
shebangStatement = this.rightPadded<JS.Shebang>({
|
|
377
|
+
kind: JS.Kind.Shebang,
|
|
378
|
+
id: randomId(),
|
|
379
|
+
prefix: emptySpace,
|
|
380
|
+
markers: emptyMarkers,
|
|
381
|
+
text: shebangText
|
|
382
|
+
}, {kind: J.Kind.Space, whitespace: afterShebang, comments: []}, emptyMarkers);
|
|
383
|
+
|
|
384
|
+
prefix = produce(prefix, draft => {
|
|
385
|
+
draft.whitespace = remainingWhitespace;
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
369
389
|
return {
|
|
370
390
|
kind: JS.Kind.CompilationUnit,
|
|
371
391
|
id: randomId(),
|
|
@@ -374,7 +394,9 @@ export class JavaScriptParserVisitor {
|
|
|
374
394
|
sourcePath: this.sourcePath,
|
|
375
395
|
charsetName: bomAndTextEncoding.encoding,
|
|
376
396
|
charsetBomMarked: bomAndTextEncoding.hasBom,
|
|
377
|
-
statements:
|
|
397
|
+
statements: shebangStatement
|
|
398
|
+
? [shebangStatement, ...this.semicolonPaddedStatementList(node.statements)]
|
|
399
|
+
: this.semicolonPaddedStatementList(node.statements),
|
|
378
400
|
eof: this.prefix(node.endOfFileToken)
|
|
379
401
|
};
|
|
380
402
|
}
|
package/src/javascript/print.ts
CHANGED
|
@@ -459,6 +459,13 @@ export class JavaScriptPrinter extends JavaScriptVisitor<PrintOutputCapture> {
|
|
|
459
459
|
return variableDeclarations;
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
+
override async visitShebang(shebang: JS.Shebang, p: PrintOutputCapture): Promise<J | undefined> {
|
|
463
|
+
await this.beforeSyntax(shebang, p);
|
|
464
|
+
p.append(shebang.text);
|
|
465
|
+
await this.afterSyntax(shebang, p);
|
|
466
|
+
return shebang;
|
|
467
|
+
}
|
|
468
|
+
|
|
462
469
|
override async visitVariableDeclarations(multiVariable: J.VariableDeclarations, p: PrintOutputCapture): Promise<J | undefined> {
|
|
463
470
|
await this.beforeSyntax(multiVariable, p);
|
|
464
471
|
await this.visitNodes(multiVariable.leadingAnnotations, p);
|
package/src/javascript/rpc.ts
CHANGED
|
@@ -247,6 +247,11 @@ class JavaScriptSender extends JavaScriptVisitor<RpcSendQueue> {
|
|
|
247
247
|
return scopedVariableDeclarations;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
override async visitShebang(shebang: JS.Shebang, q: RpcSendQueue): Promise<J | undefined> {
|
|
251
|
+
await q.getAndSend(shebang, el => el.text);
|
|
252
|
+
return shebang;
|
|
253
|
+
}
|
|
254
|
+
|
|
250
255
|
override async visitStatementExpression(statementExpression: JS.StatementExpression, q: RpcSendQueue): Promise<J | undefined> {
|
|
251
256
|
await q.getAndSend(statementExpression, el => el.statement, el => this.visit(el, q));
|
|
252
257
|
return statementExpression;
|
|
@@ -828,6 +833,13 @@ class JavaScriptReceiver extends JavaScriptVisitor<RpcReceiveQueue> {
|
|
|
828
833
|
return updateIfChanged(scopedVariableDeclarations, updates);
|
|
829
834
|
}
|
|
830
835
|
|
|
836
|
+
override async visitShebang(shebang: JS.Shebang, q: RpcReceiveQueue): Promise<J | undefined> {
|
|
837
|
+
const updates = {
|
|
838
|
+
text: await q.receive(shebang.text)
|
|
839
|
+
};
|
|
840
|
+
return updateIfChanged(shebang, updates);
|
|
841
|
+
}
|
|
842
|
+
|
|
831
843
|
override async visitStatementExpression(statementExpression: JS.StatementExpression, q: RpcReceiveQueue): Promise<J | undefined> {
|
|
832
844
|
const updates = {
|
|
833
845
|
statement: await q.receive(statementExpression.statement, el => this.visitDefined<Statement>(el, q))
|
|
@@ -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
|
/**
|
package/src/javascript/tree.ts
CHANGED
|
@@ -89,6 +89,7 @@ export namespace JS {
|
|
|
89
89
|
PropertyAssignment: "org.openrewrite.javascript.tree.JS$PropertyAssignment",
|
|
90
90
|
SatisfiesExpression: "org.openrewrite.javascript.tree.JS$SatisfiesExpression",
|
|
91
91
|
ScopedVariableDeclarations: "org.openrewrite.javascript.tree.JS$ScopedVariableDeclarations",
|
|
92
|
+
Shebang: "org.openrewrite.javascript.tree.JS$Shebang",
|
|
92
93
|
StatementExpression: "org.openrewrite.javascript.tree.JS$StatementExpression",
|
|
93
94
|
TaggedTemplateExpression: "org.openrewrite.javascript.tree.JS$TaggedTemplateExpression",
|
|
94
95
|
TemplateExpression: "org.openrewrite.javascript.tree.JS$TemplateExpression",
|
|
@@ -449,6 +450,15 @@ export namespace JS {
|
|
|
449
450
|
readonly variables: J.RightPadded<J>[];
|
|
450
451
|
}
|
|
451
452
|
|
|
453
|
+
/**
|
|
454
|
+
* Represents a shebang line at the beginning of a script.
|
|
455
|
+
* @example #!/usr/bin/env node
|
|
456
|
+
*/
|
|
457
|
+
export interface Shebang extends JS, Statement {
|
|
458
|
+
readonly kind: typeof Kind.Shebang;
|
|
459
|
+
readonly text: string;
|
|
460
|
+
}
|
|
461
|
+
|
|
452
462
|
/**
|
|
453
463
|
* Represents a statement used as an expression. The example shows a function expressions.
|
|
454
464
|
* @example const greet = function (name: string) : string { return name; };
|
|
@@ -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);
|
|
@@ -115,6 +125,14 @@ export class JavaScriptTypeMapping {
|
|
|
115
125
|
return existing;
|
|
116
126
|
}
|
|
117
127
|
|
|
128
|
+
// TypeScript represents `boolean` as a union of `false | true`, but the union
|
|
129
|
+
// type still has the Boolean flag set. Check this early to return Primitive.Boolean
|
|
130
|
+
// before we process it as a generic union type.
|
|
131
|
+
if (type.flags & ts.TypeFlags.Boolean) {
|
|
132
|
+
this.typeCache.set(signature, Type.Primitive.Boolean);
|
|
133
|
+
return Type.Primitive.Boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
118
136
|
// Get symbol for later use in type detection
|
|
119
137
|
const symbol = type.getSymbol?.();
|
|
120
138
|
|
|
@@ -248,8 +266,7 @@ export class JavaScriptTypeMapping {
|
|
|
248
266
|
|
|
249
267
|
// Check for union types (e.g., string | number)
|
|
250
268
|
if (type.flags & ts.TypeFlags.Union) {
|
|
251
|
-
|
|
252
|
-
return this.createUnionType(unionType, signature);
|
|
269
|
+
return this.createUnionType(type as ts.UnionType, signature);
|
|
253
270
|
}
|
|
254
271
|
|
|
255
272
|
// Check for intersection types (e.g., A & B)
|
|
@@ -318,16 +335,229 @@ export class JavaScriptTypeMapping {
|
|
|
318
335
|
return Type.isPrimitive(type) ? type : Type.Primitive.None;
|
|
319
336
|
}
|
|
320
337
|
|
|
321
|
-
variableType(node: ts.
|
|
338
|
+
variableType(node: ts.Node): Type.Variable | undefined {
|
|
339
|
+
let symbol: ts.Symbol | undefined;
|
|
340
|
+
let location: ts.Node = node;
|
|
341
|
+
|
|
342
|
+
// Get the symbol depending on node type
|
|
322
343
|
if (ts.isVariableDeclaration(node)) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
344
|
+
symbol = this.checker.getSymbolAtLocation(node.name);
|
|
345
|
+
} else if (ts.isParameter(node)) {
|
|
346
|
+
symbol = this.checker.getSymbolAtLocation(node.name);
|
|
347
|
+
} else if (ts.isIdentifier(node)) {
|
|
348
|
+
// For identifier references (like 'vi' in 'vi.fn()')
|
|
349
|
+
symbol = this.checker.getSymbolAtLocation(node);
|
|
350
|
+
} else if (ts.isPropertyDeclaration(node) || ts.isPropertySignature(node)) {
|
|
351
|
+
symbol = this.checker.getSymbolAtLocation(node.name);
|
|
352
|
+
} else {
|
|
353
|
+
// Not a variable/parameter/property we can handle
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (!symbol) {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Get the variable declaration (resolve aliases if needed)
|
|
362
|
+
let actualSymbol = symbol;
|
|
363
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
364
|
+
actualSymbol = this.checker.getAliasedSymbol(symbol);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Check if this symbol represents a variable, parameter, or property
|
|
368
|
+
// Exclude functions, classes, interfaces, namespaces, type aliases
|
|
369
|
+
const isExcluded = actualSymbol.flags & (
|
|
370
|
+
ts.SymbolFlags.Function |
|
|
371
|
+
ts.SymbolFlags.Class |
|
|
372
|
+
ts.SymbolFlags.Interface |
|
|
373
|
+
ts.SymbolFlags.Enum |
|
|
374
|
+
ts.SymbolFlags.ValueModule |
|
|
375
|
+
ts.SymbolFlags.NamespaceModule |
|
|
376
|
+
ts.SymbolFlags.TypeAlias |
|
|
377
|
+
ts.SymbolFlags.TypeParameter
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
if (isExcluded) {
|
|
381
|
+
// Not a variable - it's a type, function, class, namespace, etc.
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const isVariable = actualSymbol.flags & (
|
|
386
|
+
ts.SymbolFlags.Variable |
|
|
387
|
+
ts.SymbolFlags.Property |
|
|
388
|
+
ts.SymbolFlags.FunctionScopedVariable |
|
|
389
|
+
ts.SymbolFlags.BlockScopedVariable
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
if (!isVariable) {
|
|
393
|
+
// Not a variable we recognize
|
|
394
|
+
return undefined;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Get the type of the variable
|
|
398
|
+
const variableType = this.checker.getTypeOfSymbolAtLocation(actualSymbol, location);
|
|
399
|
+
const mappedType = this.getType(variableType);
|
|
400
|
+
|
|
401
|
+
// Get the owner (declaring type) for the variable
|
|
402
|
+
let ownerType: Type | undefined;
|
|
403
|
+
|
|
404
|
+
// Check if the variable is imported
|
|
405
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
406
|
+
// For imported variables, find the module specifier
|
|
407
|
+
const declarations = symbol.declarations;
|
|
408
|
+
if (declarations && declarations.length > 0) {
|
|
409
|
+
let importNode: ts.Node | undefined = declarations[0];
|
|
410
|
+
|
|
411
|
+
// Traverse up to find the ImportDeclaration
|
|
412
|
+
while (importNode && !ts.isImportDeclaration(importNode)) {
|
|
413
|
+
importNode = importNode.parent;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (importNode && ts.isImportDeclaration(importNode)) {
|
|
417
|
+
const importDecl = importNode as ts.ImportDeclaration;
|
|
418
|
+
if (ts.isStringLiteral(importDecl.moduleSpecifier)) {
|
|
419
|
+
const moduleSpecifier = importDecl.moduleSpecifier.text;
|
|
420
|
+
// Create a Type.Class representing the module
|
|
421
|
+
ownerType = Object.assign(new NonDraftableType(), {
|
|
422
|
+
kind: Type.Kind.Class,
|
|
423
|
+
flags: 0,
|
|
424
|
+
classKind: Type.Class.Kind.Interface,
|
|
425
|
+
fullyQualifiedName: moduleSpecifier,
|
|
426
|
+
typeParameters: [],
|
|
427
|
+
annotations: [],
|
|
428
|
+
interfaces: [],
|
|
429
|
+
members: [],
|
|
430
|
+
methods: [],
|
|
431
|
+
toJSON: function () {
|
|
432
|
+
return Type.signature(this);
|
|
433
|
+
}
|
|
434
|
+
}) as Type.Class;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
// For non-imported variables, check if they belong to a class/interface/namespace
|
|
440
|
+
const parentSymbol = (actualSymbol as any).parent as ts.Symbol | undefined;
|
|
441
|
+
if (parentSymbol) {
|
|
442
|
+
const parentType = this.checker.getDeclaredTypeOfSymbol(parentSymbol);
|
|
443
|
+
if (parentType) {
|
|
444
|
+
ownerType = this.getType(parentType);
|
|
445
|
+
|
|
446
|
+
// If the parent is a namespace, try to find the module it came from
|
|
447
|
+
// This handles cases like React.forwardRef where the namespace is React
|
|
448
|
+
// but the module is "react"
|
|
449
|
+
if (parentSymbol.flags & ts.SymbolFlags.ValueModule ||
|
|
450
|
+
parentSymbol.flags & ts.SymbolFlags.NamespaceModule) {
|
|
451
|
+
// Check if this namespace was imported
|
|
452
|
+
const parentDeclarations = parentSymbol.declarations;
|
|
453
|
+
if (parentDeclarations && parentDeclarations.length > 0) {
|
|
454
|
+
const firstDecl = parentDeclarations[0];
|
|
455
|
+
const sourceFile = firstDecl.getSourceFile();
|
|
456
|
+
// If it's from node_modules or a .d.ts file, try to extract the module name
|
|
457
|
+
if (sourceFile.isDeclarationFile) {
|
|
458
|
+
const fileName = sourceFile.fileName;
|
|
459
|
+
const moduleName = this.extractModuleNameFromPath(fileName);
|
|
460
|
+
if (moduleName) {
|
|
461
|
+
// Store the module as the owningClass for now
|
|
462
|
+
// (This is a bit of a hack, but works with the current type system)
|
|
463
|
+
if (Type.isClass(ownerType)) {
|
|
464
|
+
(ownerType as any).owningClass = Object.assign(new NonDraftableType(), {
|
|
465
|
+
kind: Type.Kind.Class,
|
|
466
|
+
flags: 0,
|
|
467
|
+
classKind: Type.Class.Kind.Interface,
|
|
468
|
+
fullyQualifiedName: moduleName,
|
|
469
|
+
typeParameters: [],
|
|
470
|
+
annotations: [],
|
|
471
|
+
interfaces: [],
|
|
472
|
+
members: [],
|
|
473
|
+
methods: [],
|
|
474
|
+
toJSON: function () {
|
|
475
|
+
return Type.signature(this);
|
|
476
|
+
}
|
|
477
|
+
}) as Type.Class;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
328
484
|
}
|
|
329
485
|
}
|
|
330
|
-
|
|
486
|
+
|
|
487
|
+
// Create the Type.Variable
|
|
488
|
+
const variable = Object.assign(new NonDraftableType(), {
|
|
489
|
+
kind: Type.Kind.Variable,
|
|
490
|
+
name: actualSymbol.getName(),
|
|
491
|
+
owner: ownerType,
|
|
492
|
+
type: mappedType,
|
|
493
|
+
annotations: [],
|
|
494
|
+
toJSON: function () {
|
|
495
|
+
return Type.signature(this);
|
|
496
|
+
}
|
|
497
|
+
}) as Type.Variable;
|
|
498
|
+
|
|
499
|
+
return variable;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Extract the npm module name from a file path.
|
|
504
|
+
* Handles various package manager layouts:
|
|
505
|
+
* - Standard: /path/node_modules/react/index.d.ts -> react
|
|
506
|
+
* - Scoped: /path/node_modules/@types/react/index.d.ts -> react
|
|
507
|
+
* - Scoped with __ encoding: /path/node_modules/@types/testing-library__react/index.d.ts -> @testing-library/react
|
|
508
|
+
* - Nested node_modules: /path/node_modules/pkg/node_modules/dep/index.d.ts -> dep
|
|
509
|
+
* - pnpm: /path/node_modules/.pnpm/react@18.2.0/node_modules/react/index.d.ts -> react
|
|
510
|
+
*
|
|
511
|
+
* @returns The module name, or undefined if not from node_modules
|
|
512
|
+
*/
|
|
513
|
+
private extractModuleNameFromPath(fileName: string): string | undefined {
|
|
514
|
+
if (!fileName.includes('node_modules/')) {
|
|
515
|
+
return undefined;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Find the last occurrence of node_modules/ to handle nested dependencies
|
|
519
|
+
// This also correctly handles pnpm's .pnpm structure
|
|
520
|
+
const lastNodeModulesIndex = fileName.lastIndexOf('node_modules/');
|
|
521
|
+
const afterNodeModules = fileName.substring(lastNodeModulesIndex + 'node_modules/'.length);
|
|
522
|
+
|
|
523
|
+
// Split by '/' to get path segments
|
|
524
|
+
const segments = afterNodeModules.split('/');
|
|
525
|
+
if (segments.length === 0) {
|
|
526
|
+
return undefined;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
let moduleName: string;
|
|
530
|
+
|
|
531
|
+
// Handle scoped packages (@scope/package)
|
|
532
|
+
if (segments[0].startsWith('@') && segments.length > 1) {
|
|
533
|
+
moduleName = `${segments[0]}/${segments[1]}`;
|
|
534
|
+
} else {
|
|
535
|
+
moduleName = segments[0];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Skip pnpm's .pnpm directory - it contains versioned package paths
|
|
539
|
+
// In pnpm, the actual package is in: .pnpm/pkg@version/node_modules/pkg
|
|
540
|
+
// So we already handled this by using lastIndexOf above
|
|
541
|
+
if (moduleName === '.pnpm') {
|
|
542
|
+
return undefined;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Remove @types/ prefix and decode DefinitelyTyped scoped package encoding
|
|
546
|
+
// DefinitelyTyped encodes scoped packages using __ instead of /
|
|
547
|
+
// Example: @types/testing-library__react -> @testing-library/react
|
|
548
|
+
if (moduleName.startsWith('@types/')) {
|
|
549
|
+
moduleName = moduleName.substring('@types/'.length);
|
|
550
|
+
// Decode __ encoding for scoped packages
|
|
551
|
+
// testing-library__react -> @testing-library/react
|
|
552
|
+
if (moduleName.includes('__')) {
|
|
553
|
+
const parts = moduleName.split('__');
|
|
554
|
+
if (parts.length === 2) {
|
|
555
|
+
moduleName = `@${parts[0]}/${parts[1]}`;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return moduleName;
|
|
331
561
|
}
|
|
332
562
|
|
|
333
563
|
/**
|
|
@@ -588,6 +588,14 @@ export class JavaScriptVisitor<P> extends JavaVisitor<P> {
|
|
|
588
588
|
return updateIfChanged(scopedVariableDeclarations, updates);
|
|
589
589
|
}
|
|
590
590
|
|
|
591
|
+
protected async visitShebang(shebang: JS.Shebang, p: P): Promise<J | undefined> {
|
|
592
|
+
const updates: any = {
|
|
593
|
+
prefix: await this.visitSpace(shebang.prefix, p),
|
|
594
|
+
markers: await this.visitMarkers(shebang.markers, p)
|
|
595
|
+
};
|
|
596
|
+
return updateIfChanged(shebang, updates);
|
|
597
|
+
}
|
|
598
|
+
|
|
591
599
|
protected async visitStatementExpression(statementExpression: JS.StatementExpression, p: P): Promise<J | undefined> {
|
|
592
600
|
const expression = await this.visitExpression(statementExpression, p);
|
|
593
601
|
if (!expression?.kind || expression.kind !== JS.Kind.StatementExpression) {
|
|
@@ -1158,6 +1166,8 @@ export class JavaScriptVisitor<P> extends JavaVisitor<P> {
|
|
|
1158
1166
|
return this.visitSatisfiesExpression(tree as unknown as JS.SatisfiesExpression, p);
|
|
1159
1167
|
case JS.Kind.ScopedVariableDeclarations:
|
|
1160
1168
|
return this.visitScopedVariableDeclarations(tree as unknown as JS.ScopedVariableDeclarations, p);
|
|
1169
|
+
case JS.Kind.Shebang:
|
|
1170
|
+
return this.visitShebang(tree as unknown as JS.Shebang, p);
|
|
1161
1171
|
case JS.Kind.StatementExpression:
|
|
1162
1172
|
return this.visitStatementExpression(tree as unknown as JS.StatementExpression, p);
|
|
1163
1173
|
case JS.Kind.TaggedTemplateExpression:
|
package/src/json/parser.ts
CHANGED
|
@@ -90,11 +90,25 @@ class ParseJsonReader extends ParserSourceReader {
|
|
|
90
90
|
})
|
|
91
91
|
} satisfies Json.Object as Json.Object;
|
|
92
92
|
} else if (typeof parsed === "string") {
|
|
93
|
-
|
|
93
|
+
// Extract original source to preserve escape sequences
|
|
94
|
+
const sourceStart = this.cursor;
|
|
95
|
+
this.cursor++; // skip opening quote
|
|
96
|
+
while (this.cursor < this.source.length) {
|
|
97
|
+
const char = this.source[this.cursor];
|
|
98
|
+
if (char === '\\') {
|
|
99
|
+
this.cursor += 2; // skip escape sequence
|
|
100
|
+
} else if (char === '"') {
|
|
101
|
+
this.cursor++; // skip closing quote
|
|
102
|
+
break;
|
|
103
|
+
} else {
|
|
104
|
+
this.cursor++;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const source = this.source.slice(sourceStart, this.cursor);
|
|
94
108
|
return {
|
|
95
109
|
kind: Json.Kind.Literal,
|
|
96
110
|
...base,
|
|
97
|
-
source
|
|
111
|
+
source,
|
|
98
112
|
value: parsed
|
|
99
113
|
} satisfies Json.Literal as Json.Literal;
|
|
100
114
|
} else if (typeof parsed === "number") {
|