Skip to content

Commit 816f6f3

Browse files
authored
JS: Fix type attribution for classes, methods, and FQN paths (#7195)
* JS: Fix type attribution for classes, methods, and FQN paths - Relativize absolute file paths in type FQNs using sourceRoot so method signatures don't contain machine-specific paths like /var/moderne/... - Add type attribution for ClassDeclaration, InterfaceDeclaration, EnumDeclaration, and TypeAliasDeclaration by simplifying the type() method to use getTypeAtLocation() for all non-TypeNode nodes - Add constructor type mapping in methodType() - Ensure MethodDeclaration name.type is referentially equal to methodType by computing methodType first and passing it through mapMethodName - Add FindMissingTypes test with a complex class verifying zero findings * Fix CI: add ConstructorDeclaration to createMethodType signature, fix VariableDeclarator type
1 parent a8dd796 commit 816f6f3

5 files changed

Lines changed: 396 additions & 40 deletions

File tree

rewrite-javascript/rewrite/package-lock.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rewrite-javascript/rewrite/src/javascript/parser.ts

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ export class JavaScriptParser extends Parser {
336336
// This ensures that TypeScript types with the same type.id map to the same Type instance,
337337
// preventing duplicate Type.Class, Type.Parameterized, etc. instances.
338338
const typeChecker = program.getTypeChecker();
339-
const typeMapping = new JavaScriptTypeMapping(typeChecker);
339+
const typeMapping = new JavaScriptTypeMapping(typeChecker, this.relativeTo);
340340

341341
for (const input of inputFiles.values()) {
342342
const filePath = parserInputFile(input);
@@ -1172,6 +1172,7 @@ export class JavaScriptParserVisitor {
11721172

11731173
visitMethodSignature(node: ts.MethodSignature): J.MethodDeclaration | JS.ComputedPropertyMethodDeclaration {
11741174
const prefix = this.prefix(node);
1175+
const methodType = this.mapMethodType(node);
11751176

11761177
if (ts.isComputedPropertyName(node.name)) {
11771178
return {
@@ -1187,11 +1188,10 @@ export class JavaScriptParserVisitor {
11871188
draft.markers = this.maybeAddOptionalMarker(draft, node);
11881189
}),
11891190
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
1190-
methodType: this.mapMethodType(node)
1191+
methodType: methodType
11911192
};
11921193
}
1193-
1194-
let name = this.mapMethodName(node) as J.Identifier;
1194+
let name = this.mapMethodName(node, methodType) as J.Identifier;
11951195
name = produce(name, draft => {
11961196
draft.markers = this.maybeAddOptionalMarker(draft, node);
11971197
});
@@ -1208,12 +1208,13 @@ export class JavaScriptParserVisitor {
12081208
nameAnnotations: [],
12091209
name: name,
12101210
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
1211-
methodType: this.mapMethodType(node)
1211+
methodType: methodType
12121212
};
12131213
}
12141214

12151215
visitMethodDeclaration(node: ts.MethodDeclaration): J.MethodDeclaration | JS.ComputedPropertyMethodDeclaration {
12161216
const prefix = this.prefix(node);
1217+
const methodType = this.mapMethodType(node);
12171218
const markers = produce(emptyMarkers, draft => {
12181219
if (node.asteriskToken) {
12191220
draft.markers.push({
@@ -1224,7 +1225,7 @@ export class JavaScriptParserVisitor {
12241225
}
12251226
});
12261227

1227-
let name = this.mapMethodName(node);
1228+
let name = this.mapMethodName(node, methodType);
12281229
name = produce(name, draft => {
12291230
draft.markers = this.maybeAddOptionalMarker(draft, node);
12301231
});
@@ -1242,7 +1243,7 @@ export class JavaScriptParserVisitor {
12421243
name: name as ComputedPropertyName,
12431244
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
12441245
body: node.body && this.convert<J.Block>(node.body),
1245-
methodType: this.mapMethodType(node)
1246+
methodType: methodType
12461247
};
12471248
}
12481249

@@ -1259,16 +1260,29 @@ export class JavaScriptParserVisitor {
12591260
name: name as J.Identifier,
12601261
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
12611262
body: node.body && this.convert<J.Block>(node.body),
1262-
methodType: this.mapMethodType(node)
1263+
methodType: methodType
12631264
};
12641265
}
12651266

1266-
private mapMethodName(node: ts.NamedDeclaration): J.Identifier | JS.ComputedPropertyName {
1267-
return !node.name
1268-
? this.mapIdentifier(node, "")
1269-
: ts.isStringLiteral(node.name) || ts.isNumericLiteral(node.name)
1270-
? this.mapIdentifier(node.name, node.name.getText())
1271-
: this.visit(node.name);
1267+
private mapMethodName(node: ts.NamedDeclaration, methodType?: Type.Method): J.Identifier | JS.ComputedPropertyName {
1268+
// Build the name identifier without an independent type — the containing
1269+
// MethodDeclaration's methodType is the authoritative source. We set the
1270+
// identifier's type to the same methodType object to maintain the
1271+
// referential equality invariant (name.type === methodType).
1272+
let ident: J.Identifier | JS.ComputedPropertyName;
1273+
if (!node.name) {
1274+
ident = this.mapIdentifier(node, "", false);
1275+
} else if (ts.isStringLiteral(node.name) || ts.isNumericLiteral(node.name)) {
1276+
ident = this.mapIdentifier(node.name, node.name.getText(), false);
1277+
} else if (ts.isComputedPropertyName(node.name)) {
1278+
return this.visit(node.name);
1279+
} else {
1280+
ident = this.mapIdentifier(node.name, node.name.getText(), false);
1281+
}
1282+
if (methodType && ident.kind === J.Kind.Identifier) {
1283+
return {...ident, type: methodType};
1284+
}
1285+
return ident;
12721286
}
12731287

12741288
private mapTypeInfo(node: ts.MethodDeclaration | ts.PropertyDeclaration | ts.VariableDeclaration | ts.ParameterDeclaration
@@ -1306,6 +1320,7 @@ export class JavaScriptParserVisitor {
13061320
// using string literal for the following case: class A { "constructor"() {} }
13071321
const constructorKeyword = node.getChildren()
13081322
.find(n => (n.kind === ts.SyntaxKind.ConstructorKeyword) || ((n.kind === ts.SyntaxKind.StringLiteral) && (n.getText().includes("constructor"))))!;
1323+
const methodType = this.mapMethodType(node);
13091324
return {
13101325
kind: J.Kind.MethodDeclaration,
13111326
id: randomId(),
@@ -1314,15 +1329,16 @@ export class JavaScriptParserVisitor {
13141329
leadingAnnotations: this.mapDecorators(node),
13151330
modifiers: this.mapModifiers(node),
13161331
nameAnnotations: [],
1317-
name: this.mapIdentifier(constructorKeyword, constructorKeyword.getText()),
1332+
name: {...this.mapIdentifier(constructorKeyword, constructorKeyword.getText(), false), type: methodType},
13181333
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
13191334
body: node.body && this.convert<J.Block>(node.body),
1320-
methodType: this.mapMethodType(node)
1335+
methodType: methodType
13211336
};
13221337
}
13231338

13241339
visitGetAccessor(node: ts.GetAccessorDeclaration): J.MethodDeclaration | JS.ComputedPropertyMethodDeclaration {
1325-
const name = this.mapMethodName(node);
1340+
const methodType = this.mapMethodType(node);
1341+
const name = this.mapMethodName(node, methodType);
13261342
if (ts.isComputedPropertyName(node.name)) {
13271343
return {
13281344
kind: JS.Kind.ComputedPropertyMethodDeclaration,
@@ -1335,7 +1351,7 @@ export class JavaScriptParserVisitor {
13351351
name: name as ComputedPropertyName,
13361352
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
13371353
body: node.body && this.convert<J.Block>(node.body),
1338-
methodType: this.mapMethodType(node)
1354+
methodType: methodType
13391355
};
13401356
}
13411357

@@ -1351,12 +1367,13 @@ export class JavaScriptParserVisitor {
13511367
name: name as J.Identifier,
13521368
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
13531369
body: node.body && this.convert<J.Block>(node.body),
1354-
methodType: this.mapMethodType(node)
1370+
methodType: methodType
13551371
};
13561372
}
13571373

13581374
visitSetAccessor(node: ts.SetAccessorDeclaration): J.MethodDeclaration | JS.ComputedPropertyMethodDeclaration {
1359-
const name = this.mapMethodName(node);
1375+
const methodType = this.mapMethodType(node);
1376+
const name = this.mapMethodName(node, methodType);
13601377
if (ts.isComputedPropertyName(node.name)) {
13611378
return {
13621379
kind: JS.Kind.ComputedPropertyMethodDeclaration,
@@ -1368,7 +1385,7 @@ export class JavaScriptParserVisitor {
13681385
name: name as ComputedPropertyName,
13691386
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
13701387
body: node.body && this.convert<J.Block>(node.body),
1371-
methodType: this.mapMethodType(node)
1388+
methodType: methodType
13721389
};
13731390
}
13741391

@@ -1383,11 +1400,12 @@ export class JavaScriptParserVisitor {
13831400
name: name as J.Identifier,
13841401
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
13851402
body: node.body && this.convert<J.Block>(node.body),
1386-
methodType: this.mapMethodType(node)
1403+
methodType: methodType
13871404
};
13881405
}
13891406

13901407
visitCallSignature(node: ts.CallSignatureDeclaration): J.MethodDeclaration {
1408+
const methodType = this.mapMethodType(node);
13911409
return {
13921410
kind: J.Kind.MethodDeclaration,
13931411
id: randomId(),
@@ -1401,17 +1419,19 @@ export class JavaScriptParserVisitor {
14011419
name: {
14021420
kind: J.Kind.Identifier,
14031421
id: randomId(),
1404-
prefix: emptySpace/* this.prefix(node.getChildren().find(n => n.kind === ts.SyntaxKind.OpenBraceToken)!) */,
1422+
prefix: emptySpace,
14051423
markers: emptyMarkers,
1406-
annotations: [], // FIXME decorators
1424+
annotations: [],
14071425
simpleName: "",
1426+
type: methodType
14081427
},
14091428
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
1410-
methodType: this.mapMethodType(node)
1429+
methodType: methodType
14111430
};
14121431
}
14131432

14141433
visitConstructSignature(node: ts.ConstructSignatureDeclaration): J.MethodDeclaration {
1434+
const methodType = this.mapMethodType(node);
14151435
return {
14161436
kind: J.Kind.MethodDeclaration,
14171437
id: randomId(),
@@ -1428,10 +1448,11 @@ export class JavaScriptParserVisitor {
14281448
prefix: emptySpace,
14291449
markers: emptyMarkers,
14301450
annotations: [],
1431-
simpleName: 'new'
1451+
simpleName: 'new',
1452+
type: methodType
14321453
},
14331454
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
1434-
methodType: this.mapMethodType(node)
1455+
methodType: methodType
14351456
};
14361457
}
14371458

@@ -3318,6 +3339,7 @@ export class JavaScriptParserVisitor {
33183339
}
33193340

33203341
private mapFunctionDeclaration(node: ts.FunctionDeclaration | ts.FunctionExpression): J.MethodDeclaration {
3342+
const methodType = this.mapMethodType(node);
33213343
return {
33223344
kind: J.Kind.MethodDeclaration,
33233345
id: randomId(),
@@ -3340,12 +3362,12 @@ export class JavaScriptParserVisitor {
33403362
leadingAnnotations: [],
33413363
nameAnnotations: [],
33423364
modifiers: this.mapModifiers(node),
3343-
name: this.mapMethodName(node) as J.Identifier,
3365+
name: this.mapMethodName(node, methodType) as J.Identifier,
33443366
typeParameters: this.mapTypeParametersAsObject(node),
33453367
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
33463368
returnTypeExpression: this.mapTypeInfo(node),
33473369
body: node.body && this.convert<J.Block>(node.body),
3348-
methodType: this.mapMethodType(node)
3370+
methodType: methodType
33493371
};
33503372
}
33513373

@@ -3830,19 +3852,20 @@ export class JavaScriptParserVisitor {
38303852
}
38313853

38323854
visitExternalModuleReference(node: ts.ExternalModuleReference): J.MethodInvocation {
3855+
const methodType = this.mapMethodType(node);
38333856
return {
38343857
kind: J.Kind.MethodInvocation,
38353858
id: randomId(),
38363859
prefix: this.prefix(node),
38373860
markers: emptyMarkers,
3838-
name: this.mapIdentifier(node, "require"),
3861+
name: {...this.mapIdentifier(node, "require", false), type: methodType},
38393862
arguments: {
38403863
kind: J.Kind.Container,
38413864
before: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenParenToken)!),
38423865
elements: [this.rightPadded(this.visit(node.expression), this.suffix(node.expression))],
38433866
markers: emptyMarkers
38443867
},
3845-
methodType: this.mapMethodType(node)
3868+
methodType: methodType
38463869
}
38473870
}
38483871

rewrite-javascript/rewrite/src/javascript/type-mapping.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* limitations under the License.
1515
*/
1616
import ts from "typescript";
17+
import * as path from "path";
1718
import {Type} from "../java";
1819
import FUNCTION_TYPE_NAME = Type.FUNCTION_TYPE_NAME;
1920

@@ -30,7 +31,8 @@ export class JavaScriptTypeMapping {
3031

3132

3233
constructor(
33-
private readonly checker: ts.TypeChecker
34+
private readonly checker: ts.TypeChecker,
35+
private readonly sourceRoot?: string
3436
) {
3537
this.regExpSymbol = checker.resolveName(
3638
"RegExp",
@@ -69,11 +71,14 @@ export class JavaScriptTypeMapping {
6971
// Fall through to regular type checking if not a variable
7072
}
7173

74+
// TypeNode needs getTypeFromTypeNode (resolves the annotation itself).
75+
// Everything else — expressions, declarations, specifiers, bindings, etc. —
76+
// uses getTypeAtLocation which works for virtually all node kinds.
7277
let type: ts.Type | undefined;
73-
if (ts.isExpression(node)) {
74-
type = this.checker.getTypeAtLocation(node);
75-
} else if (ts.isTypeNode(node)) {
78+
if (ts.isTypeNode(node)) {
7679
type = this.checker.getTypeFromTypeNode(node);
80+
} else {
81+
type = this.checker.getTypeAtLocation(node);
7782
}
7883
return type && this.getType(type);
7984
}
@@ -565,7 +570,7 @@ export class JavaScriptTypeMapping {
565570
*/
566571
private createMethodType(
567572
signature: ts.Signature,
568-
node: ts.MethodSignature | ts.FunctionDeclaration | ts.MethodDeclaration | ts.FunctionExpression | ts.CallExpression | ts.NewExpression,
573+
node: ts.MethodSignature | ts.FunctionDeclaration | ts.MethodDeclaration | ts.ConstructorDeclaration | ts.FunctionExpression | ts.CallExpression | ts.NewExpression,
569574
declaringType: Type.FullyQualified,
570575
name: string,
571576
declaredFormalTypeNames: string[] = []
@@ -915,6 +920,22 @@ export class JavaScriptTypeMapping {
915920
declaredFormalTypeNames.push(tp.name.getText());
916921
}
917922
}
923+
} else if (ts.isConstructorDeclaration(node)) {
924+
// For constructor declarations
925+
signature = this.checker.getSignatureFromDeclaration(node);
926+
if (!signature) {
927+
return undefined;
928+
}
929+
930+
methodName = "<constructor>";
931+
932+
const parent = node.parent;
933+
if (ts.isClassDeclaration(parent) || ts.isClassExpression(parent)) {
934+
const parentType = this.checker.getTypeAtLocation(parent);
935+
declaringType = this.getType(parentType) as Type.FullyQualified;
936+
} else {
937+
declaringType = Type.unknownType as Type.FullyQualified;
938+
}
918939
} else if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)) {
919940
// For function declarations/expressions
920941
signature = this.checker.getSignatureFromDeclaration(node);
@@ -1032,6 +1053,14 @@ export class JavaScriptTypeMapping {
10321053
cleanedName = packageName;
10331054
}
10341055
}
1056+
} else if (path.isAbsolute(cleanedName)) {
1057+
// TypeScript returns absolute file paths as module names for project source files
1058+
// that are ES modules (have imports/exports). Relativize using sourceRoot.
1059+
// Example: /var/moderne/.../BookStack/resources/js/foo.MyClass
1060+
// Should become: resources/js/foo.MyClass
1061+
if (this.sourceRoot) {
1062+
cleanedName = path.relative(this.sourceRoot, cleanedName);
1063+
}
10351064
}
10361065

10371066
return cleanedName.endsWith('Constructor') ?

rewrite-javascript/rewrite/test/javascript/parser/function-type.test.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,18 @@ describe("function type mapping", () => {
9595
let foundMultiply = false;
9696

9797
await (new class extends JavaScriptVisitor<any> {
98-
async visitIdentifier(identifier: J.Identifier, _: any): Promise<J | undefined> {
99-
if (identifier.simpleName === 'multiply' && identifier.type) {
98+
async visitMethodDeclaration(method: J.MethodDeclaration, _: any): Promise<J | undefined> {
99+
if (method.name.simpleName === 'multiply' && method.methodType) {
100100
foundMultiply = true;
101-
assertFunctionType(identifier.type, 2, ['x', 'y'], 'double');
101+
expect(method.methodType.name).toBe('multiply');
102+
expect(method.methodType.parameterNames).toEqual(['x', 'y']);
103+
expect(method.methodType.parameterTypes.length).toBe(2);
104+
expect(Type.isPrimitive(method.methodType.returnType)).toBeTruthy();
105+
expect((method.methodType.returnType as Type.Primitive).keyword).toBe('double');
106+
// name.type should be referentially equal to methodType
107+
expect(method.name.type).toBe(method.methodType);
102108
}
103-
return identifier;
109+
return super.visitMethodDeclaration(method, _);
104110
}
105111
}).visit(cu, 0);
106112

0 commit comments

Comments
 (0)