Ensure locations are propagated everywhere

There were a few places where we didn't propagage source locations
correctly.  This fixes that.  Part of the issue was we didn't have
good helper methods to propagate "ranges" (e.g., a sequence of
statements or expressions), so this change adds those.

This change also establishes a new naming convention; the addLocation*
methods adds a newly allocated location based on translating a TypeScript
AST node, whereas the copyLocation* methods copy them directly from
other MuIL nodes.  Thus, there are now four methods: addLocation,
addLocationRange, copyLocation, and copyLocationRange.
This commit is contained in:
joeduffy 2017-01-11 08:31:03 -08:00
parent 6c3826659c
commit b3247a37cd
2 changed files with 123 additions and 61 deletions

View file

@ -133,6 +133,15 @@ function isComputed(name: ts.Node | undefined): boolean {
return false;
}
// Translates a TypeScript position into a MuIL position (0 to 1 based lines).
function mapPosition(s: ts.SourceFile, p: number): ast.Position {
let lc = s.getLineAndCharacterOfPosition(p);
return <ast.Position>{
line: lc.line + 1, // transform to 1-based line number
column: lc.character,
};
}
// A transpiler is responsible for transforming TypeScript program artifacts into MuPack/MuIL AST forms.
export class Transpiler {
private meta: pack.Metadata; // the package's metadata.
@ -175,36 +184,63 @@ export class Transpiler {
/** Helpers **/
// This annotates a given MuPack/MuIL node with another TypeScript node's source position information.
private copyLocation<T extends ast.Node>(src: ts.Node, dst: T): T {
let pos = (s: ts.SourceFile, p: number) => {
// Translate a TypeScript position into a MuIL position (0 to 1 based lines).
let lc = s.getLineAndCharacterOfPosition(p);
return {
line: lc.line + 1, // transform to 1-based line number
column: lc.character,
};
};
private addLocation<T extends ast.Node>(src: ts.Node, dst: T): T {
// Turn the source file name into one relative to the current root path.
let s: ts.SourceFile = src.getSourceFile();
let relativePath: string = fspath.relative(this.comp.root, s.fileName);
dst.loc = {
file: relativePath,
start: pos(s, src.getStart()),
end: pos(s, src.getEnd()),
start: mapPosition(s, src.getStart()),
end: mapPosition(s, src.getEnd()),
};
// Despite mutating in place, we return the node to facilitate a more fluent style.
return dst;
}
// This annotates a given MuPack/MuIL node with a range of TypeScript node source positions.
private addLocationRange<T extends ast.Node>(start: ts.Node, end: ts.Node, dst: T): T {
contract.assert(start.getSourceFile() === end.getSourceFile());
// Turn the source file name into one relative to the current root path.
let s: ts.SourceFile = start.getSourceFile();
let relativePath: string = fspath.relative(this.comp.root, s.fileName);
dst.loc = {
file: relativePath,
start: mapPosition(s, start.getStart()),
end: mapPosition(s, end.getEnd()),
};
// Despite mutating in place, we return the node to facilitate a more fluent style.
return dst;
}
// This just carries forward an existing location from another node.
private copyLocation<T extends ast.Node>(src: ast.Node, dst: T): T {
dst.loc = src.loc;
return dst;
}
// This just carries forward an existing location range from another node.
private copyLocationRange<T extends ast.Node>(start: ast.Node, end: ast.Node, dst: T): T {
let sloc: ast.Location | undefined = start.loc || end.loc;
let eloc: ast.Location | undefined = end.loc || start.loc;
if (sloc !== undefined && eloc !== undefined) {
contract.assert(sloc.file === eloc.file);
dst.loc = {
file: sloc.file,
start: sloc.start,
end: eloc.end,
};
}
return dst;
}
/** Transformations **/
/** Symbols **/
private transformIdentifier(node: ts.Identifier): ast.Identifier {
return this.copyLocation(node, ident(node.text));
return this.addLocation(node, ident(node.text));
}
/** Modules **/
@ -268,7 +304,7 @@ export class Transpiler {
moduleName = moduleName.substring(0, moduleExtIndex);
}
return this.copyLocation(node, <ast.Module>{
return this.addLocation(node, <ast.Module>{
kind: ast.moduleKind,
name: ident(moduleName),
members: members,
@ -440,7 +476,7 @@ export class Transpiler {
if (statement.kind === ast.blockKind) {
return <ast.Block>statement;
}
return this.copyLocation(node, <ast.Block>{
return this.addLocation(node, <ast.Block>{
kind: ast.blockKind,
statements: [ statement ],
});
@ -518,7 +554,7 @@ export class Transpiler {
}
let mods: ts.ModifierFlags = ts.getCombinedModifierFlags(node);
return this.copyLocation(node, <ast.Class>{
return this.addLocation(node, <ast.Class>{
kind: ast.classKind,
name: name,
access: access,
@ -626,7 +662,7 @@ export class Transpiler {
private transformModuleFunctionDeclaration(
node: ts.FunctionDeclaration, access: symbols.Accessibility): ast.ModuleMethod {
let decl: FunctionLikeDeclaration = this.transformFunctionLikeDeclaration(node);
return this.copyLocation(node, <ast.ModuleMethod>{
return this.addLocation(node, <ast.ModuleMethod>{
kind: ast.moduleMethodKind,
name: decl.name,
access: access,
@ -675,7 +711,7 @@ export class Transpiler {
private makeVariableInitializer(decl: VariableDeclaration<ast.Variable>): ast.Statement {
contract.requires(!!decl.initializer, "decl", "Expected variable declaration to have an initializer");
return this.copyLocation(decl.node, {
return this.addLocation(decl.node, {
kind: ast.binaryOperatorExpressionKind,
left: <ast.LoadLocationExpression>{
kind: ast.loadLocationExpressionKind,
@ -727,14 +763,21 @@ export class Transpiler {
statements.push(this.makeVariableInitializer(vdecl));
}
}
if (statements.length === 1) {
return statements[0];
contract.assert(statements.length > 0);
if (statements.length > 1) {
return this.copyLocationRange(
statements[0],
statements[statements.length-1],
<ast.MultiStatement>{
kind: ast.multiStatementKind,
statements: statements,
},
);
}
else {
return <ast.MultiStatement>{
kind: ast.multiStatementKind,
statements: statements,
};
return statements[0];
}
}
@ -823,7 +866,7 @@ export class Transpiler {
let mods: ts.ModifierFlags = ts.getCombinedModifierFlags(node);
let decl: FunctionLikeDeclaration = this.transformFunctionLikeDeclaration(node);
return this.copyLocation(node, <ast.ClassMethod>{
return this.addLocation(node, <ast.ClassMethod>{
kind: ast.classMethodKind,
name: decl.name,
access: this.getClassAccessibility(node),
@ -861,7 +904,7 @@ export class Transpiler {
/** Control flow statements **/
private transformBreakStatement(node: ts.BreakStatement): ast.BreakStatement {
return this.copyLocation(node, <ast.BreakStatement>{
return this.addLocation(node, <ast.BreakStatement>{
kind: ast.breakStatementKind,
label: object.maybeUndefined(node.label, (id: ts.Identifier) => this.transformIdentifier(id)),
});
@ -876,7 +919,7 @@ export class Transpiler {
}
private transformContinueStatement(node: ts.ContinueStatement): ast.ContinueStatement {
return this.copyLocation(node, <ast.ContinueStatement>{
return this.addLocation(node, <ast.ContinueStatement>{
kind: ast.continueStatementKind,
label: object.maybeUndefined(node.label, (id: ts.Identifier) => this.transformIdentifier(id)),
});
@ -885,7 +928,7 @@ export class Transpiler {
// This transforms a higher-level TypeScript `do`/`while` statement by expanding into an ordinary `while` statement.
private transformDoStatement(node: ts.DoStatement): ast.WhileStatement {
// Now create a new block that simply concatenates the existing one with a test/`break`.
let body: ast.Block = this.copyLocation(node.statement, {
let body: ast.Block = this.addLocation(node.statement, {
kind: ast.blockKind,
statements: [
this.transformStatement(node.statement),
@ -903,7 +946,7 @@ export class Transpiler {
],
});
return this.copyLocation(node, <ast.WhileStatement>{
return this.addLocation(node, <ast.WhileStatement>{
kind: ast.whileStatementKind,
test: <ast.BoolLiteral>{
kind: ast.boolLiteralKind,
@ -930,7 +973,7 @@ export class Transpiler {
}
private transformIfStatement(node: ts.IfStatement): ast.IfStatement {
return this.copyLocation(node, <ast.IfStatement>{
return this.addLocation(node, <ast.IfStatement>{
kind: ast.ifStatementKind,
condition: this.transformExpression(node.expression),
consequent: this.transformStatement(node.thenStatement),
@ -940,7 +983,7 @@ export class Transpiler {
}
private transformReturnStatement(node: ts.ReturnStatement): ast.ReturnStatement {
return this.copyLocation(node, <ast.ReturnStatement>{
return this.addLocation(node, <ast.ReturnStatement>{
kind: ast.returnStatementKind,
expression: object.maybeUndefined(
node.expression, (expr: ts.Expression) => this.transformExpression(expr)),
@ -952,7 +995,7 @@ export class Transpiler {
}
private transformThrowStatement(node: ts.ThrowStatement): ast.ThrowStatement {
return this.copyLocation(node, <ast.ThrowStatement>{
return this.addLocation(node, <ast.ThrowStatement>{
kind: ast.throwStatementKind,
expression: this.transformExpression(node.expression),
});
@ -963,7 +1006,7 @@ export class Transpiler {
}
private transformWhileStatement(node: ts.WhileStatement): ast.WhileStatement {
return this.copyLocation(node, <ast.WhileStatement>{
return this.addLocation(node, <ast.WhileStatement>{
kind: ast.whileStatementKind,
test: this.transformExpression(node.expression),
body: this.transformStatementAsBlock(node.statement),
@ -974,7 +1017,7 @@ export class Transpiler {
private transformBlock(node: ts.Block): ast.Block {
// TODO(joe): map directives.
return this.copyLocation(node, <ast.Block>{
return this.addLocation(node, <ast.Block>{
kind: ast.blockKind,
statements: node.statements.map((stmt: ts.Statement) => this.transformStatement(stmt)),
});
@ -983,26 +1026,26 @@ export class Transpiler {
private transformDebuggerStatement(node: ts.DebuggerStatement): ast.Statement {
// The debugger statement in ECMAScript can be used to trip a breakpoint. We don't have the equivalent in Mu at
// the moment, so we simply produce an empty statement in its place.
return this.copyLocation(node, <ast.Statement>{
return this.addLocation(node, <ast.Statement>{
kind: ast.emptyStatementKind,
});
}
private transformEmptyStatement(node: ts.EmptyStatement): ast.EmptyStatement {
return this.copyLocation(node, <ast.EmptyStatement>{
return this.addLocation(node, <ast.EmptyStatement>{
kind: ast.emptyStatementKind,
});
}
private transformExpressionStatement(node: ts.ExpressionStatement): ast.ExpressionStatement {
return this.copyLocation(node, <ast.ExpressionStatement>{
return this.addLocation(node, <ast.ExpressionStatement>{
kind: ast.expressionStatementKind,
expression: this.transformExpression(node.expression),
});
}
private transformLabeledStatement(node: ts.LabeledStatement): ast.LabeledStatement {
return this.copyLocation(node, <ast.LabeledStatement>{
return this.addLocation(node, <ast.LabeledStatement>{
kind: ast.labeledStatementKind,
label: this.transformIdentifier(node.label),
statement: this.transformStatement(node.statement),
@ -1120,7 +1163,7 @@ export class Transpiler {
// TODO: finish binary operator mapping; for any left that are unsupported, introduce a real error message.
return contract.fail(`Unsupported binary operator: ${ts.SyntaxKind[node.operatorToken.kind]}`);
}
return this.copyLocation(node, <ast.BinaryOperatorExpression>{
return this.addLocation(node, <ast.BinaryOperatorExpression>{
kind: ast.binaryOperatorExpressionKind,
operator: operator,
left: this.transformExpression(node.left),
@ -1130,6 +1173,8 @@ export class Transpiler {
private transformBinarySequenceExpression(node: ts.BinaryExpression): ast.SequenceExpression {
contract.assert(node.operatorToken.kind === ts.SyntaxKind.CommaToken);
// Pile up the expressions in the right order.
let curr: ts.Expression = node;
let binary: ts.BinaryExpression = node;
let expressions: ast.Expression[] = [];
@ -1139,14 +1184,20 @@ export class Transpiler {
curr = binary.left;
}
expressions.unshift(this.transformExpression(curr));
return <ast.SequenceExpression>{
kind: ast.sequenceExpressionKind,
expressions: expressions,
};
contract.assert(expressions.length > 0);
return this.copyLocationRange(
expressions[0],
expressions[expressions.length-1],
<ast.SequenceExpression>{
kind: ast.sequenceExpressionKind,
expressions: expressions,
},
);
}
private transformCallExpression(node: ts.CallExpression): ast.InvokeFunctionExpression {
return this.copyLocation(node, <ast.InvokeFunctionExpression>{
return this.addLocation(node, <ast.InvokeFunctionExpression>{
kind: ast.invokeFunctionExpressionKind,
function: this.transformExpression(node.expression),
arguments: node.arguments.map((expr: ts.Expression) => this.transformExpression(expr)),
@ -1158,7 +1209,7 @@ export class Transpiler {
}
private transformConditionalExpression(node: ts.ConditionalExpression): ast.ConditionalExpression {
return this.copyLocation(node, <ast.ConditionalExpression>{
return this.addLocation(node, <ast.ConditionalExpression>{
kind: ast.conditionalExpressionKind,
condition: this.transformExpression(node.condition),
consequent: this.transformExpression(node.whenTrue),
@ -1175,13 +1226,13 @@ export class Transpiler {
if (node.argumentExpression) {
switch (node.argumentExpression.kind) {
case ts.SyntaxKind.Identifier:
return this.copyLocation(node, <ast.LoadLocationExpression>{
return this.addLocation(node, <ast.LoadLocationExpression>{
kind: ast.loadLocationExpressionKind,
object: object,
name: this.transformIdentifier(<ts.Identifier>node.argumentExpression),
});
default:
return this.copyLocation(node, <ast.LoadDynamicExpression>{
return this.addLocation(node, <ast.LoadDynamicExpression>{
kind: ast.loadDynamicExpressionKind,
object: object,
name: this.transformExpression(<ts.Expression>node.argumentExpression),
@ -1218,7 +1269,7 @@ export class Transpiler {
private transformPostfixUnaryExpression(node: ts.PostfixUnaryExpression): ast.UnaryOperatorExpression {
let operator: ast.UnaryOperator | undefined = postfixUnaryOperators.get(node.operator);
contract.assert(!!(operator = operator!));
return this.copyLocation(node, <ast.UnaryOperatorExpression>{
return this.addLocation(node, <ast.UnaryOperatorExpression>{
kind: ast.unaryOperatorExpressionKind,
postfix: true,
operator: operator,
@ -1229,7 +1280,7 @@ export class Transpiler {
private transformPrefixUnaryExpression(node: ts.PrefixUnaryExpression): ast.UnaryOperatorExpression {
let operator: ast.UnaryOperator | undefined = prefixUnaryOperators.get(node.operator);
contract.assert(!!(operator = operator!));
return this.copyLocation(node, <ast.UnaryOperatorExpression>{
return this.addLocation(node, <ast.UnaryOperatorExpression>{
kind: ast.unaryOperatorExpressionKind,
postfix: false,
operator: operator,
@ -1238,7 +1289,7 @@ export class Transpiler {
}
private transformPropertyAccessExpression(node: ts.PropertyAccessExpression): ast.LoadLocationExpression {
return this.copyLocation(node, <ast.LoadLocationExpression>{
return this.addLocation(node, <ast.LoadLocationExpression>{
kind: ast.loadLocationExpressionKind,
object: this.transformExpression(node.expression),
name: this.transformIdentifier(node.name),
@ -1255,11 +1306,11 @@ export class Transpiler {
return contract.fail("New T(..) expression must have an identifier T");
}
return <ast.NewExpression>{
return this.addLocation(node, <ast.NewExpression>{
kind: ast.newExpressionKind,
type: ty,
arguments: node.arguments.map((expr: ts.Expression) => this.transformExpression(expr)),
};
});
}
private transformOmittedExpression(node: ts.OmittedExpression): ast.Expression {
@ -1275,7 +1326,7 @@ export class Transpiler {
}
private transformSuperExpression(node: ts.SuperExpression): ast.LoadLocationExpression {
return this.copyLocation(node, <ast.LoadLocationExpression>{
return this.addLocation(node, <ast.LoadLocationExpression>{
kind: ast.loadLocationExpressionKind,
name: ident(symbols.specialVariableSuper),
});
@ -1290,7 +1341,7 @@ export class Transpiler {
}
private transformThisExpression(node: ts.ThisExpression): ast.LoadLocationExpression {
return this.copyLocation(node, <ast.LoadLocationExpression>{
return this.addLocation(node, <ast.LoadLocationExpression>{
kind: ast.loadLocationExpressionKind,
name: ident(symbols.specialVariableThis),
});
@ -1312,7 +1363,7 @@ export class Transpiler {
private transformBooleanLiteral(node: ts.BooleanLiteral): ast.BoolLiteral {
contract.assert(node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.TrueKeyword);
return this.copyLocation(node, <ast.BoolLiteral>{
return this.addLocation(node, <ast.BoolLiteral>{
kind: ast.boolLiteralKind,
raw: node.getText(),
value: (node.kind === ts.SyntaxKind.TrueKeyword),
@ -1324,14 +1375,14 @@ export class Transpiler {
}
private transformNullLiteral(node: ts.NullLiteral): ast.NullLiteral {
return this.copyLocation(node, <ast.NullLiteral>{
return this.addLocation(node, <ast.NullLiteral>{
kind: ast.nullLiteralKind,
raw: node.getText(),
});
}
private transformNumericLiteral(node: ts.NumericLiteral): ast.NumberLiteral {
return this.copyLocation(node, <ast.NumberLiteral>{
return this.addLocation(node, <ast.NumberLiteral>{
kind: ast.numberLiteralKind,
raw: node.text,
value: Number(node.text),
@ -1346,7 +1397,7 @@ export class Transpiler {
// TODO: we need to dynamically populate the resulting object with ECMAScript-style string functions. It's not
// yet clear how to do this in a way that facilitates inter-language interoperability. This is especially
// challenging because most use of such functions will be entirely dynamic.
return this.copyLocation(node, <ast.StringLiteral>{
return this.addLocation(node, <ast.StringLiteral>{
kind: ast.stringLiteralKind,
raw: node.text,
value: node.text,
@ -1382,7 +1433,7 @@ export class Transpiler {
}
private transformIdentifierExpression(node: ts.Identifier): ast.Identifier {
return this.copyLocation(node, ident(node.text));
return this.addLocation(node, ident(node.text));
}
private transformObjectBindingPattern(node: ts.ObjectBindingPattern): ast.Expression {

View file

@ -561,7 +561,18 @@
}
}
}
]
],
"loc": {
"file": "index.ts",
"start": {
"line": 14,
"column": 15
},
"end": {
"line": 14,
"column": 60
}
}
},
"loc": {
"file": "index.ts",