From 806a442894c82d111485ad46add240cb5e713b6f Mon Sep 17 00:00:00 2001 From: Julian Rosse Date: Thu, 14 Feb 2019 22:51:33 -0500 Subject: [PATCH] If AST (#5160) * updated grammar * updated grammar * tests * location data tests * fix from code review --- lib/coffeescript/grammar.js | 8 +- lib/coffeescript/nodes.js | 133 ++-- lib/coffeescript/parser.js | 2 +- src/grammar.coffee | 8 +- src/nodes.coffee | 130 ++-- test/abstract_syntax_tree.coffee | 185 ++++-- .../abstract_syntax_tree_location_data.coffee | 567 ++++++++++++++++++ 7 files changed, 887 insertions(+), 146 deletions(-) diff --git a/lib/coffeescript/grammar.js b/lib/coffeescript/grammar.js index 69a76f26..dea30bd7 100644 --- a/lib/coffeescript/grammar.js +++ b/lib/coffeescript/grammar.js @@ -1991,7 +1991,7 @@ LOC(1)(Block.wrap([$1])), { type: $2, - statement: true + postfix: true }); }), o('Expression POST_IF Expression', @@ -2000,7 +2000,7 @@ LOC(1)(Block.wrap([$1])), { type: $2, - statement: true + postfix: true }); }) ], @@ -2035,7 +2035,7 @@ LOC(1)(Block.wrap([$1])), { type: $2, - statement: true + postfix: true }); }), o('Expression POST_IF ExpressionLine', @@ -2044,7 +2044,7 @@ LOC(1)(Block.wrap([$1])), { type: $2, - statement: true + postfix: true }); }) ], diff --git a/lib/coffeescript/nodes.js b/lib/coffeescript/nodes.js index d31ac48b..607561c6 100644 --- a/lib/coffeescript/nodes.js +++ b/lib/coffeescript/nodes.js @@ -363,7 +363,11 @@ // as JSON. This is what the `ast` option in the Node API returns. // We try to follow the [Babel AST spec](https://github.com/babel/babel/blob/master/packages/babel-parser/ast/spec.md) // as closely as possible, for improved interoperability with other tools. - ast(o) { + ast(o, level) { + o = Object.assign({}, o); + if (level != null) { + o.level = level; + } // Every abstract syntax tree node object has four categories of properties: // - type, stored in the `type` field and a string like `NumberLiteral`. // - location data, stored in the `loc`, `start`, `end` and `range` fields. @@ -373,7 +377,7 @@ // `parsedValue` are all top level fields in the AST node object. We have // separate methods for returning each category, that we merge together here. return Object.assign({}, { - type: this.astType() + type: this.astType(o) }, this.astProperties(o), this.astLocationData()); } @@ -699,6 +703,7 @@ } ast(o) { + o.level = LEVEL_TOP; this.initializeScope(o); return super.ast(o); } @@ -1457,8 +1462,8 @@ return [this.makeCode('['), ...this.value.compileToFragments(o, LEVEL_LIST), this.makeCode(']')]; } - ast(o) { - return this.value.ast(o); + ast(o, level) { + return this.value.ast(o, level); } }; @@ -1636,7 +1641,7 @@ astProperties(o) { var ref1, ref2; return { - argument: (ref1 = (ref2 = this.expression) != null ? ref2.ast(o) : void 0) != null ? ref1 : null + argument: (ref1 = (ref2 = this.expression) != null ? ref2.ast(o, LEVEL_PAREN) : void 0) != null ? ref1 : null }; } @@ -1673,11 +1678,11 @@ } } - ast(o) { + ast(o, level) { this.checkScope(o); return new Op(this.keyword, new Return(this.expression).withLocationDataFrom(this.expression != null ? { locationData: mergeLocationData(this.returnKeyword.locationData, this.expression.locationData) - } : this.returnKeyword)).withLocationDataFrom(this).ast(o); + } : this.returnKeyword)).withLocationDataFrom(this).ast(o, level); } }; @@ -1990,15 +1995,15 @@ return object; } - ast(o) { + ast(o, level) { if (!this.hasProperties()) { // If the `Value` has no properties, the AST node is just whatever this // node’s `base` is. - return this.base.ast(o); + return this.base.ast(o, level); } // Otherwise, call `Base::ast` which in turn calls the `astType` and // `astProperties` methods below. - return super.ast(o); + return super.ast(o, level); } astType() { @@ -2019,7 +2024,7 @@ property.name.csx = true; } return { - object: this.object().ast(o), + object: this.object().ast(o, LEVEL_ACCESS), property: property.ast(o), computed: property instanceof Index || !(((ref2 = property.name) != null ? ref2.unwrap() : void 0) instanceof PropertyName), optional: !!property.soak, @@ -2260,13 +2265,13 @@ return fragments; } - ast() { + ast(o) { var attribute, j, len1, ref1, results; ref1 = this.attributes; results = []; for (j = 0, len1 = ref1.length; j < len1; j++) { attribute = ref1[j]; - results.push(attribute.ast()); + results.push(attribute.ast(o)); } return results; } @@ -2315,7 +2320,7 @@ return !this.tagName.base.value.length; } - ast(o) { + ast(o, level) { var tagName; // The location data spanning the opening element < ... > is captured by // the generated Arr which contains the element's attributes @@ -2325,7 +2330,7 @@ if (this.content != null) { this.closingElementLocationData = mergeAstLocationData(jisonLocationDataToAstLocationData(tagName.closingTagOpeningBracketLocationData), jisonLocationDataToAstLocationData(tagName.closingTagClosingBracketLocationData)); } - return super.ast(o); + return super.ast(o, level); } astType() { @@ -2601,14 +2606,14 @@ astProperties(o) { var arg; return { - callee: this.variable.ast(o), + callee: this.variable.ast(o, LEVEL_ACCESS), arguments: (function() { var j, len1, ref1, results; ref1 = this.args; results = []; for (j = 0, len1 = ref1.length; j < len1; j++) { arg = ref1[j]; - results.push(arg.ast(o)); + results.push(arg.ast(o, LEVEL_LIST)); } return results; }).call(this), @@ -2795,11 +2800,11 @@ } } - ast(o) { + ast(o, level) { // Babel doesn’t have an AST node for `Access`, but rather just includes // this Access node’s child `name` Identifier node as the `property` of // the `MemberExpression` node. - return this.name.ast(o); + return this.name.ast(o, level); } }; @@ -2830,13 +2835,13 @@ return this.index.shouldCache(); } - ast(o) { + ast(o, level) { // Babel doesn’t have an AST node for `Index`, but rather just includes // this Index node’s child `index` Identifier node as the `property` of // the `MemberExpression` node. The fact that the `MemberExpression`’s // `property` is an Index means that `computed` is `true` for the // `MemberExpression`. - return this.index.ast(o); + return this.index.ast(o, level); } }; @@ -3019,8 +3024,8 @@ return [this.makeCode(`.slice(${fragmentsToText(fromCompiled)}${toStr || ''})`)]; } - ast(o) { - return this.range.ast(o); + ast(o, level) { + return this.range.ast(o, level); } }; @@ -3568,7 +3573,7 @@ results = []; for (j = 0, len1 = ref1.length; j < len1; j++) { object = ref1[j]; - results.push(object.ast(o)); + results.push(object.ast(o, LEVEL_LIST)); } return results; }).call(this) @@ -4863,8 +4868,8 @@ astProperties(o) { var ref1, ret; ret = { - right: this.value.ast(o), - left: this.variable.ast(o) + right: this.value.ast(o, LEVEL_LIST), + left: this.variable.ast(o, LEVEL_LIST) }; if (!this.isDefaultAssignment()) { ret.operator = (ref1 = this.originalContext) != null ? ref1 : '='; @@ -5341,9 +5346,9 @@ return results; } - ast(o) { + ast(o, level) { this.updateOptions(o); - return super.ast(o); + return super.ast(o, level); } astType() { @@ -5389,7 +5394,7 @@ } return results; }).call(this), - body: this.body.ast(o), + body: this.body.ast(o, LEVEL_TOP), generator: !!this.isGenerator, async: !!this.isAsync, // We never generate named functions, so specify `id` as `null`, which @@ -5617,7 +5622,7 @@ astProperties(o) { return { - argument: this.name.ast(o), + argument: this.name.ast(o, LEVEL_OP), postfix: this.postfix }; } @@ -6078,11 +6083,11 @@ return super.toString(idt, this.constructor.name + ' ' + this.operator); } - ast(o) { + ast(o, level) { if (this.isYield() || this.isAwait()) { this.checkContinuation(o); } - return super.ast(o); + return super.ast(o, level); } astType() { @@ -6111,8 +6116,8 @@ astProperties(o) { var argument, firstAst, ref1, secondAst; - firstAst = this.first.ast(o); - secondAst = (ref1 = this.second) != null ? ref1.ast(o) : void 0; + firstAst = this.first.ast(o, LEVEL_OP); + secondAst = (ref1 = this.second) != null ? ref1.ast(o, LEVEL_OP) : void 0; switch (false) { case !this.isUnary(): argument = this.isYield() && this.first.unwrap().value === '' ? null : firstAst; @@ -6290,10 +6295,10 @@ astProperties(o) { var ref1, ref2; return { - block: this.attempt.ast(o), + block: this.attempt.ast(o, LEVEL_TOP), handler: (ref1 = (ref2 = this.catch) != null ? ref2.ast(o) : void 0) != null ? ref1 : null, // Include `finally` keyword in location data. - finalizer: this.ensure != null ? Object.assign(this.ensure.ast(o), mergeAstLocationData(jisonLocationDataToAstLocationData(this.finallyTag.locationData), this.ensure.astLocationData())) : null + finalizer: this.ensure != null ? Object.assign(this.ensure.ast(o, LEVEL_TOP), mergeAstLocationData(jisonLocationDataToAstLocationData(this.finallyTag.locationData), this.ensure.astLocationData())) : null }; } @@ -6355,7 +6360,7 @@ var ref1, ref2; return { param: (ref1 = (ref2 = this.errorVariable) != null ? ref2.ast(o) : void 0) != null ? ref1 : null, - body: this.recovery.ast(o) + body: this.recovery.ast(o, LEVEL_TOP) }; } @@ -6394,7 +6399,7 @@ astProperties(o) { return { - argument: this.expression.ast(o) + argument: this.expression.ast(o, LEVEL_LIST) }; } @@ -6535,7 +6540,7 @@ } ast(o) { - return this.body.unwrap().ast(o); + return this.body.unwrap().ast(o, LEVEL_PAREN); } }; @@ -7083,7 +7088,7 @@ astProperties(o) { var ref1, ref2; return { - discriminant: (ref1 = (ref2 = this.subject) != null ? ref2.ast(o) : void 0) != null ? ref1 : null, + discriminant: (ref1 = (ref2 = this.subject) != null ? ref2.ast(o, LEVEL_PAREN) : void 0) != null ? ref1 : null, cases: this.casesAst(o) }; } @@ -7110,8 +7115,8 @@ astProperties(o) { var ref1, ref2, ref3, ref4; return { - test: (ref1 = (ref2 = this.test) != null ? ref2.ast(o) : void 0) != null ? ref1 : null, - consequent: (ref3 = (ref4 = this.block) != null ? ref4.ast(o).body : void 0) != null ? ref3 : [], + test: (ref1 = (ref2 = this.test) != null ? ref2.ast(o, LEVEL_PAREN) : void 0) != null ? ref1 : null, + consequent: (ref3 = (ref4 = this.block) != null ? ref4.ast(o, LEVEL_TOP).body : void 0) != null ? ref3 : [], trailing: !!this.trailing }; } @@ -7149,13 +7154,13 @@ // because ternaries are already proper expressions, and don’t need conversion. exports.If = If = (function() { class If extends Base { - constructor(condition, body1, options = {}) { + constructor(condition1, body1, options = {}) { super(); + this.condition = condition1; this.body = body1; - this.condition = options.type === 'unless' ? condition.invert() : condition; this.elseBody = null; this.isChain = false; - ({soak: this.soak} = options); + ({soak: this.soak, postfix: this.postfix, type: this.type} = options); if (this.condition.comments) { moveComments(this.condition, this); } @@ -7175,10 +7180,14 @@ addElse(elseBody) { if (this.isChain) { this.elseBodyNode().addElse(elseBody); + this.locationData = mergeLocationData(this.locationData, this.elseBodyNode().locationData); } else { this.isChain = elseBody instanceof If; this.elseBody = this.ensureBlock(elseBody); this.elseBody.updateLocationDataIfMissing(elseBody.locationData); + if ((this.locationData != null) && (this.elseBody.locationData != null)) { + this.locationData = mergeLocationData(this.locationData, this.elseBody.locationData); + } } return this; } @@ -7227,12 +7236,12 @@ child = del(o, 'chainChild'); exeq = del(o, 'isExistentialEquals'); if (exeq) { - return new If(this.condition.invert(), this.elseBodyNode(), { + return new If(this.processedCondition().invert(), this.elseBodyNode(), { type: 'if' }).compileToFragments(o); } indent = o.indent + TAB; - cond = this.condition.compileToFragments(o, LEVEL_PAREN); + cond = this.processedCondition().compileToFragments(o, LEVEL_PAREN); body = this.ensureBlock(this.body).compileToFragments(merge(o, {indent})); ifPart = [].concat(this.makeCode("if ("), cond, this.makeCode(") {\n"), body, this.makeCode(`\n${this.tab}}`)); if (!child) { @@ -7254,7 +7263,7 @@ // Compile the `If` as a conditional operator. compileExpression(o) { var alt, body, cond, fragments; - cond = this.condition.compileToFragments(o, LEVEL_COND); + cond = this.processedCondition().compileToFragments(o, LEVEL_COND); body = this.bodyNode().compileToFragments(o, LEVEL_LIST); alt = this.elseBodyNode() ? this.elseBodyNode().compileToFragments(o, LEVEL_LIST) : [this.makeCode('void 0')]; fragments = cond.concat(this.makeCode(" ? "), body, this.makeCode(" : "), alt); @@ -7269,6 +7278,34 @@ return this.soak && this; } + processedCondition() { + return this.processedConditionCache != null ? this.processedConditionCache : this.processedConditionCache = this.type === 'unless' ? this.condition.invert() : this.condition; + } + + isStatementAst(o) { + return o.level === LEVEL_TOP; + } + + astType(o) { + if (this.isStatementAst(o)) { + return 'IfStatement'; + } else { + return 'ConditionalExpression'; + } + } + + astProperties(o) { + var isStatement, ref1, ref2; + isStatement = this.isStatementAst(o); + return { + test: this.condition.ast(o, isStatement ? LEVEL_PAREN : LEVEL_COND), + consequent: isStatement ? this.body.ast(o, LEVEL_TOP) : this.bodyNode().ast(o, LEVEL_TOP), + alternate: this.isChain ? this.elseBody.unwrap().ast(o, isStatement ? LEVEL_TOP : LEVEL_COND) : (ref1 = (ref2 = this.elseBody) != null ? ref2.ast(o, LEVEL_TOP) : void 0) != null ? ref1 : null, + postfix: !!this.postfix, + inverted: this.type === 'unless' + }; + } + }; If.prototype.children = ['condition', 'body', 'elseBody']; diff --git a/lib/coffeescript/parser.js b/lib/coffeescript/parser.js index fa14bfe4..c0342bee 100644 --- a/lib/coffeescript/parser.js +++ b/lib/coffeescript/parser.js @@ -984,7 +984,7 @@ this.$ = yy.addDataToNode(yy, _$[$0-2], $$[$0-2], _$[$0], $$[$0], true)(new yy.I yy.addDataToNode(yy, _$[$0-2], $$[$0-2], null, null, true)(yy.Block.wrap([$$[$0-2]])), { type: $$[$0-1], - statement: true + postfix: true })); break; case 360: diff --git a/src/grammar.coffee b/src/grammar.coffee index 2c1a2190..a5117d58 100644 --- a/src/grammar.coffee +++ b/src/grammar.coffee @@ -817,8 +817,8 @@ grammar = If: [ o 'IfBlock' o 'IfBlock ELSE Block', -> $1.addElse $3 - o 'Statement POST_IF Expression', -> new If $3, LOC(1)(Block.wrap [$1]), type: $2, statement: true - o 'Expression POST_IF Expression', -> new If $3, LOC(1)(Block.wrap [$1]), type: $2, statement: true + o 'Statement POST_IF Expression', -> new If $3, LOC(1)(Block.wrap [$1]), type: $2, postfix: true + o 'Expression POST_IF Expression', -> new If $3, LOC(1)(Block.wrap [$1]), type: $2, postfix: true ] IfBlockLine: [ @@ -829,8 +829,8 @@ grammar = IfLine: [ o 'IfBlockLine' o 'IfBlockLine ELSE Block', -> $1.addElse $3 - o 'Statement POST_IF ExpressionLine', -> new If $3, LOC(1)(Block.wrap [$1]), type: $2, statement: true - o 'Expression POST_IF ExpressionLine', -> new If $3, LOC(1)(Block.wrap [$1]), type: $2, statement: true + o 'Statement POST_IF ExpressionLine', -> new If $3, LOC(1)(Block.wrap [$1]), type: $2, postfix: true + o 'Expression POST_IF ExpressionLine', -> new If $3, LOC(1)(Block.wrap [$1]), type: $2, postfix: true ] # Arithmetic and logical operators, working on one or more operands. diff --git a/src/nodes.coffee b/src/nodes.coffee index 20162df2..4568a562 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -270,7 +270,9 @@ exports.Base = class Base # as JSON. This is what the `ast` option in the Node API returns. # We try to follow the [Babel AST spec](https://github.com/babel/babel/blob/master/packages/babel-parser/ast/spec.md) # as closely as possible, for improved interoperability with other tools. - ast: (o) -> + ast: (o, level) -> + o = Object.assign {}, o + o.level = level if level? # Every abstract syntax tree node object has four categories of properties: # - type, stored in the `type` field and a string like `NumberLiteral`. # - location data, stored in the `loc`, `start`, `end` and `range` fields. @@ -279,7 +281,7 @@ exports.Base = class Base # These fields are all intermixed in the Babel spec; `type` and `start` and # `parsedValue` are all top level fields in the AST node object. We have # separate methods for returning each category, that we merge together here. - Object.assign {}, {type: @astType()}, @astProperties(o), @astLocationData() + Object.assign {}, {type: @astType(o)}, @astProperties(o), @astLocationData() # By default, a node class has no specific properties. astProperties: -> {} @@ -498,6 +500,7 @@ exports.Root = class Root extends Base o.scope.parameter name for name in o.locals or [] ast: (o) -> + o.level = LEVEL_TOP @initializeScope o super o @@ -1011,8 +1014,8 @@ exports.ComputedPropertyName = class ComputedPropertyName extends PropertyName compileNode: (o) -> [@makeCode('['), @value.compileToFragments(o, LEVEL_LIST)..., @makeCode(']')] - ast: (o) -> - @value.ast o + ast: (o, level) -> + @value.ast o, level exports.StatementLiteral = class StatementLiteral extends Literal isStatement: YES @@ -1121,7 +1124,7 @@ exports.Return = class Return extends Base astType: -> 'ReturnStatement' astProperties: (o) -> - argument: @expression?.ast(o) ? null + argument: @expression?.ast(o, LEVEL_PAREN) ? null # Parent class for `YieldReturn`/`AwaitReturn`. exports.FuncDirectiveReturn = class FuncDirectiveReturn extends Return @@ -1138,7 +1141,7 @@ exports.FuncDirectiveReturn = class FuncDirectiveReturn extends Return isStatementAst: NO - ast: (o) -> + ast: (o, level) -> @checkScope o new Op @keyword, @@ -1150,7 +1153,7 @@ exports.FuncDirectiveReturn = class FuncDirectiveReturn extends Return @returnKeyword ) .withLocationDataFrom @ - .ast o + .ast o, level # `yield return` works exactly like `return`, except that it turns the function # into a generator. @@ -1339,13 +1342,13 @@ exports.Value = class Value extends Base mergeLocationData @base.locationData, initialProperties[initialProperties.length - 1].locationData object - ast: (o) -> + ast: (o, level) -> # If the `Value` has no properties, the AST node is just whatever this # node’s `base` is. - return @base.ast o unless @hasProperties() + return @base.ast o, level unless @hasProperties() # Otherwise, call `Base::ast` which in turn calls the `astType` and # `astProperties` methods below. - super o + super o, level astType: -> if @isCSXTag() @@ -1360,7 +1363,7 @@ exports.Value = class Value extends Base [..., property] = @properties property.name.csx = yes if @isCSXTag() return - object: @object().ast o + object: @object().ast o, LEVEL_ACCESS property: property.ast o computed: property instanceof Index or property.name?.unwrap() not instanceof PropertyName optional: !!property.soak @@ -1519,8 +1522,8 @@ exports.CSXAttributes = class CSXAttributes extends Base fragments.push attribute.compileToFragments(o, LEVEL_TOP)... fragments - ast: -> - attribute.ast() for attribute in @attributes + ast: (o) -> + attribute.ast(o) for attribute in @attributes # Node for a CSX element exports.CSXElement = class CSXElement extends Base @@ -1545,7 +1548,7 @@ exports.CSXElement = class CSXElement extends Base isFragment: -> !@tagName.base.value.length - ast: (o) -> + ast: (o, level) -> # The location data spanning the opening element < ... > is captured by # the generated Arr which contains the element's attributes @openingElementLocationData = jisonLocationDataToAstLocationData @attributes.locationData @@ -1558,7 +1561,7 @@ exports.CSXElement = class CSXElement extends Base jisonLocationDataToAstLocationData tagName.closingTagClosingBracketLocationData ) - super o + super o, level astType: -> if @isFragment() @@ -1769,8 +1772,8 @@ exports.Call = class Call extends Base astProperties: (o) -> return - callee: @variable.ast o - arguments: arg.ast(o) for arg in @args + callee: @variable.ast o, LEVEL_ACCESS + arguments: arg.ast(o, LEVEL_LIST) for arg in @args optional: !!@soak implicit: !!@implicit @@ -1890,11 +1893,11 @@ exports.Access = class Access extends Base shouldCache: NO - ast: (o) -> + ast: (o, level) -> # Babel doesn’t have an AST node for `Access`, but rather just includes # this Access node’s child `name` Identifier node as the `property` of # the `MemberExpression` node. - @name.ast o + @name.ast o, level #### Index @@ -1911,13 +1914,13 @@ exports.Index = class Index extends Base shouldCache: -> @index.shouldCache() - ast: (o) -> + ast: (o, level) -> # Babel doesn’t have an AST node for `Index`, but rather just includes # this Index node’s child `index` Identifier node as the `property` of # the `MemberExpression` node. The fact that the `MemberExpression`’s # `property` is an Index means that `computed` is `true` for the # `MemberExpression`. - @index.ast o + @index.ast o, level #### Range @@ -2074,8 +2077,8 @@ exports.Slice = class Slice extends Base "+#{fragmentsToText compiled} + 1 || 9e9" [@makeCode ".slice(#{ fragmentsToText fromCompiled }#{ toStr or '' })"] - ast: (o) -> - @range.ast(o) + ast: (o, level) -> + @range.ast(o, level) #### Obj @@ -2388,7 +2391,7 @@ exports.Arr = class Arr extends Base astProperties: (o) -> return elements: - object.ast(o) for object in @objects + object.ast(o, LEVEL_LIST) for object in @objects #### Class @@ -3263,8 +3266,8 @@ exports.Assign = class Assign extends Base astProperties: (o) -> ret = - right: @value.ast o - left: @variable.ast o + right: @value.ast o, LEVEL_LIST + left: @variable.ast o, LEVEL_LIST unless @isDefaultAssignment() ret.operator = @originalContext ? '=' @@ -3599,9 +3602,9 @@ exports.Code = class Code extends Base for {name} in @params when name instanceof Arr or name instanceof Obj name.propagateLhs yes - ast: (o) -> + ast: (o, level) -> @updateOptions o - super o + super o, level astType: -> if @bound @@ -3624,7 +3627,7 @@ exports.Code = class Code extends Base astProperties: (o) -> return params: @paramForAst(param).ast(o) for param in @params - body: @body.ast o + body: @body.ast o, LEVEL_TOP generator: !!@isGenerator async: !!@isAsync # We never generate named functions, so specify `id` as `null`, which @@ -3772,7 +3775,7 @@ exports.Splat = class Splat extends Base 'SpreadElement' astProperties: (o) -> { - argument: @name.ast o + argument: @name.ast o, LEVEL_OP @postfix } @@ -4097,9 +4100,9 @@ exports.Op = class Op extends Base toString: (idt) -> super idt, @constructor.name + ' ' + @operator - ast: (o) -> + ast: (o, level) -> @checkContinuation o if @isYield() or @isAwait() - super o + super o, level astType: -> return 'AwaitExpression' if @isAwait() @@ -4112,8 +4115,8 @@ exports.Op = class Op extends Base else 'BinaryExpression' astProperties: (o) -> - firstAst = @first.ast o - secondAst = @second?.ast o + firstAst = @first.ast o, LEVEL_OP + secondAst = @second?.ast o, LEVEL_OP switch when @isUnary() argument = @@ -4219,11 +4222,11 @@ exports.Try = class Try extends Base astProperties: (o) -> return - block: @attempt.ast o + block: @attempt.ast o, LEVEL_TOP handler: @catch?.ast(o) ? null finalizer: if @ensure? - Object.assign @ensure.ast(o), + Object.assign @ensure.ast(o, LEVEL_TOP), # Include `finally` keyword in location data. mergeAstLocationData( jisonLocationDataToAstLocationData(@finallyTag.locationData), @@ -4263,7 +4266,7 @@ exports.Catch = class Catch extends Base astProperties: (o) -> return param: @errorVariable?.ast(o) ? null - body: @recovery.ast o + body: @recovery.ast o, LEVEL_TOP #### Throw @@ -4291,7 +4294,7 @@ exports.Throw = class Throw extends Base astProperties: (o) -> return - argument: @expression.ast o + argument: @expression.ast o, LEVEL_LIST #### Existence @@ -4380,7 +4383,7 @@ exports.Parens = class Parens extends Base return @wrapInBraces fragments if @csxAttribute if bare then fragments else @wrapInParentheses fragments - ast: (o) -> @body.unwrap().ast o + ast: (o) -> @body.unwrap().ast o, LEVEL_PAREN #### StringWithInterpolations @@ -4703,7 +4706,7 @@ exports.Switch = class Switch extends Base astProperties: (o) -> return - discriminant: @subject?.ast(o) ? null + discriminant: @subject?.ast(o, LEVEL_PAREN) ? null cases: @casesAst o class SwitchCase extends Base @@ -4714,8 +4717,8 @@ class SwitchCase extends Base astProperties: (o) -> return - test: @test?.ast(o) ? null - consequent: @block?.ast(o).body ? [] + test: @test?.ast(o, LEVEL_PAREN) ? null + consequent: @block?.ast(o, LEVEL_TOP).body ? [] trailing: !!@trailing exports.SwitchWhen = class SwitchWhen extends Base @@ -4732,12 +4735,11 @@ exports.SwitchWhen = class SwitchWhen extends Base # Single-expression **Ifs** are compiled into conditional operators if possible, # because ternaries are already proper expressions, and don’t need conversion. exports.If = class If extends Base - constructor: (condition, @body, options = {}) -> + constructor: (@condition, @body, options = {}) -> super() - @condition = if options.type is 'unless' then condition.invert() else condition @elseBody = null @isChain = false - {@soak} = options + {@soak, @postfix, @type} = options moveComments @condition, @ if @condition.comments children: ['condition', 'body', 'elseBody'] @@ -4749,10 +4751,12 @@ exports.If = class If extends Base addElse: (elseBody) -> if @isChain @elseBodyNode().addElse elseBody + @locationData = mergeLocationData @locationData, @elseBodyNode().locationData else @isChain = elseBody instanceof If @elseBody = @ensureBlock elseBody @elseBody.updateLocationDataIfMissing elseBody.locationData + @locationData = mergeLocationData @locationData, @elseBody.locationData if @locationData? and @elseBody.locationData? this # The **If** only compiles into a statement if either of its bodies needs @@ -4782,10 +4786,10 @@ exports.If = class If extends Base exeq = del o, 'isExistentialEquals' if exeq - return new If(@condition.invert(), @elseBodyNode(), type: 'if').compileToFragments o + return new If(@processedCondition().invert(), @elseBodyNode(), type: 'if').compileToFragments o indent = o.indent + TAB - cond = @condition.compileToFragments o, LEVEL_PAREN + cond = @processedCondition().compileToFragments o, LEVEL_PAREN body = @ensureBlock(@body).compileToFragments merge o, {indent} ifPart = [].concat @makeCode("if ("), cond, @makeCode(") {\n"), body, @makeCode("\n#{@tab}}") ifPart.unshift @makeCode @tab unless child @@ -4800,7 +4804,7 @@ exports.If = class If extends Base # Compile the `If` as a conditional operator. compileExpression: (o) -> - cond = @condition.compileToFragments o, LEVEL_COND + cond = @processedCondition().compileToFragments o, LEVEL_COND body = @bodyNode().compileToFragments o, LEVEL_LIST alt = if @elseBodyNode() then @elseBodyNode().compileToFragments(o, LEVEL_LIST) else [@makeCode('void 0')] fragments = cond.concat @makeCode(" ? "), body, @makeCode(" : "), alt @@ -4809,6 +4813,36 @@ exports.If = class If extends Base unfoldSoak: -> @soak and this + processedCondition: -> + @processedConditionCache ?= if @type is 'unless' then @condition.invert() else @condition + + isStatementAst: (o) -> + o.level is LEVEL_TOP + + astType: (o) -> + if @isStatementAst o + 'IfStatement' + else + 'ConditionalExpression' + + astProperties: (o) -> + isStatement = @isStatementAst o + + return + test: @condition.ast o, if isStatement then LEVEL_PAREN else LEVEL_COND + consequent: + if isStatement + @body.ast o, LEVEL_TOP + else + @bodyNode().ast o, LEVEL_TOP + alternate: + if @isChain + @elseBody.unwrap().ast o, if isStatement then LEVEL_TOP else LEVEL_COND + else + @elseBody?.ast(o, LEVEL_TOP) ? null + postfix: !!@postfix + inverted: @type is 'unless' + # Constants # --------- diff --git a/test/abstract_syntax_tree.coffee b/test/abstract_syntax_tree.coffee index 03395a1b..61ec4192 100644 --- a/test/abstract_syntax_tree.coffee +++ b/test/abstract_syntax_tree.coffee @@ -2321,48 +2321,151 @@ test "AST as expected for Switch node", -> # # TODO: File issue for compile error when using `then` or `;` where `\n` is rn. -# test "AST as expected for If node", -> -# testExpression 'if maybe then yes', -# type: 'If' -# isChain: no -# condition: -# type: 'IdentifierLiteral' -# body: -# type: 'Value' -# base: -# type: 'BooleanLiteral' +test "AST as expected for If node", -> + testStatement 'if maybe then yes', + type: 'IfStatement' + test: ID 'maybe' + consequent: + type: 'BlockStatement' + body: [ + type: 'ExpressionStatement' + expression: + type: 'BooleanLiteral' + ] + alternate: null + postfix: no + inverted: no -# testExpression 'yes if maybe', -# type: 'If' -# isChain: no -# condition: -# type: 'IdentifierLiteral' -# body: -# type: 'Value' -# base: -# type: 'BooleanLiteral' + testStatement 'yes if maybe', + type: 'IfStatement' + test: ID 'maybe' + consequent: + type: 'BlockStatement' + body: [ + type: 'ExpressionStatement' + expression: + type: 'BooleanLiteral' + ] + alternate: null + postfix: yes + inverted: no -# # TODO: Where's the post-if flag? + testStatement 'unless x then x else if y then y else z', + type: 'IfStatement' + test: ID 'x' + consequent: + type: 'BlockStatement' + body: [ + type: 'ExpressionStatement' + expression: ID 'x' + ] + alternate: + type: 'IfStatement' + test: ID 'y' + consequent: + type: 'BlockStatement' + body: [ + type: 'ExpressionStatement' + expression: ID 'y' + ] + alternate: + type: 'BlockStatement' + body: [ + type: 'ExpressionStatement' + expression: ID 'z' + ] + postfix: no + inverted: no + postfix: no + inverted: yes -# testExpression 'unless x then x else if y then y else z', -# type: 'If' -# isChain: yes -# condition: -# type: 'Op' -# operator: '!' -# originalOperator: '!' -# flip: no -# body: -# type: 'Value' -# elseBody: -# type: 'If' -# isChain: no -# condition: -# type: 'IdentifierLiteral' -# body: -# type: 'Value' -# elseBody: -# type: 'Value' -# isDefaultValue: no + testStatement ''' + if a + b + else + if c + d + ''', + type: 'IfStatement' + test: ID 'a' + consequent: + type: 'BlockStatement' + body: [ + type: 'ExpressionStatement' + expression: ID 'b' + ] + alternate: + type: 'BlockStatement' + body: [ + type: 'IfStatement' + test: ID 'c' + consequent: + type: 'BlockStatement' + body: [ + type: 'ExpressionStatement' + expression: ID 'd' + ] + alternate: null + postfix: no + inverted: no + ] + postfix: no + inverted: no -# # TODO: AST generator should preserve use of `unless`. + testExpression ''' + a = + if b then c else if d then e + ''', + type: 'AssignmentExpression' + right: + type: 'ConditionalExpression' + test: ID 'b' + consequent: ID 'c' + alternate: + type: 'ConditionalExpression' + test: ID 'd' + consequent: ID 'e' + alternate: null + postfix: no + inverted: no + postfix: no + inverted: no + + testExpression ''' + f( + if b + c + d + ) + ''', + type: 'CallExpression' + arguments: [ + type: 'ConditionalExpression' + test: ID 'b' + consequent: + type: 'BlockStatement' + body: [ + type: 'ExpressionStatement' + expression: + ID 'c' + , + type: 'ExpressionStatement' + expression: + ID 'd' + ] + postfix: no + inverted: no + ] + + testStatement 'a unless b', + type: 'IfStatement' + test: ID 'b' + consequent: + type: 'BlockStatement' + body: [ + type: 'ExpressionStatement' + expression: ID 'a' + ] + alternate: null + postfix: yes + inverted: yes diff --git a/test/abstract_syntax_tree_location_data.coffee b/test/abstract_syntax_tree_location_data.coffee index ab0bf138..1cf15760 100644 --- a/test/abstract_syntax_tree_location_data.coffee +++ b/test/abstract_syntax_tree_location_data.coffee @@ -3889,3 +3889,570 @@ test "AST as expected for AwaitReturn node", -> end: line: 1 column: 15 + +test "AST as expected for If node", -> + testAstLocationData 'if maybe then yes', + type: 'IfStatement' + test: + start: 3 + end: 8 + range: [3, 8] + loc: + start: + line: 1 + column: 3 + end: + line: 1 + column: 8 + consequent: + body: [ + expression: + start: 14 + end: 17 + range: [14, 17] + loc: + start: + line: 1 + column: 14 + end: + line: 1 + column: 17 + start: 14 + end: 17 + range: [14, 17] + loc: + start: + line: 1 + column: 14 + end: + line: 1 + column: 17 + ] + start: 8 + end: 17 + range: [8, 17] + loc: + start: + line: 1 + column: 8 + end: + line: 1 + column: 17 + start: 0 + end: 17 + range: [0, 17] + loc: + start: + line: 1 + column: 0 + end: + line: 1 + column: 17 + + testAstLocationData 'yes if maybe', + type: 'IfStatement' + test: + start: 7 + end: 12 + range: [7, 12] + loc: + start: + line: 1 + column: 7 + end: + line: 1 + column: 12 + consequent: + body: [ + expression: + start: 0 + end: 3 + range: [0, 3] + loc: + start: + line: 1 + column: 0 + end: + line: 1 + column: 3 + start: 0 + end: 3 + range: [0, 3] + loc: + start: + line: 1 + column: 0 + end: + line: 1 + column: 3 + ] + start: 0 + end: 3 + range: [0, 3] + loc: + start: + line: 1 + column: 0 + end: + line: 1 + column: 3 + start: 0 + end: 12 + range: [0, 12] + loc: + start: + line: 1 + column: 0 + end: + line: 1 + column: 12 + + testAstLocationData 'unless x then x else if y then y else z', + type: 'IfStatement' + test: + start: 7 + end: 8 + range: [7, 8] + loc: + start: + line: 1 + column: 7 + end: + line: 1 + column: 8 + consequent: + body: [ + expression: + start: 14 + end: 15 + range: [14, 15] + loc: + start: + line: 1 + column: 14 + end: + line: 1 + column: 15 + start: 14 + end: 15 + range: [14, 15] + loc: + start: + line: 1 + column: 14 + end: + line: 1 + column: 15 + ] + start: 8 + end: 15 + range: [8, 15] + loc: + start: + line: 1 + column: 8 + end: + line: 1 + column: 15 + alternate: + test: + start: 24 + end: 25 + range: [24, 25] + loc: + start: + line: 1 + column: 24 + end: + line: 1 + column: 25 + consequent: + body: [ + expression: + start: 31 + end: 32 + range: [31, 32] + loc: + start: + line: 1 + column: 31 + end: + line: 1 + column: 32 + start: 31 + end: 32 + range: [31, 32] + loc: + start: + line: 1 + column: 31 + end: + line: 1 + column: 32 + ] + start: 25 + end: 32 + range: [25, 32] + loc: + start: + line: 1 + column: 25 + end: + line: 1 + column: 32 + alternate: + body: [ + expression: + start: 38 + end: 39 + range: [38, 39] + loc: + start: + line: 1 + column: 38 + end: + line: 1 + column: 39 + start: 38 + end: 39 + range: [38, 39] + loc: + start: + line: 1 + column: 38 + end: + line: 1 + column: 39 + ] + start: 37 + end: 39 + range: [37, 39] + loc: + start: + line: 1 + column: 37 + end: + line: 1 + column: 39 + start: 21 + end: 39 + range: [21, 39] + loc: + start: + line: 1 + column: 21 + end: + line: 1 + column: 39 + start: 0 + end: 39 + range: [0, 39] + loc: + start: + line: 1 + column: 0 + end: + line: 1 + column: 39 + + testAstLocationData ''' + if a + b + else + if c + d + ''', + type: 'IfStatement' + test: + start: 3 + end: 4 + range: [3, 4] + loc: + start: + line: 1 + column: 3 + end: + line: 1 + column: 4 + consequent: + body: [ + expression: + start: 7 + end: 8 + range: [7, 8] + loc: + start: + line: 2 + column: 2 + end: + line: 2 + column: 3 + start: 7 + end: 8 + range: [7, 8] + loc: + start: + line: 2 + column: 2 + end: + line: 2 + column: 3 + ] + start: 5 + end: 8 + range: [5, 8] + loc: + start: + line: 2 + column: 0 + end: + line: 2 + column: 3 + alternate: + body: [ + test: + start: 19 + end: 20 + range: [19, 20] + loc: + start: + line: 4 + column: 5 + end: + line: 4 + column: 6 + consequent: + body: [ + expression: + start: 25 + end: 26 + range: [25, 26] + loc: + start: + line: 5 + column: 4 + end: + line: 5 + column: 5 + start: 25 + end: 26 + range: [25, 26] + loc: + start: + line: 5 + column: 4 + end: + line: 5 + column: 5 + ] + start: 21 + end: 26 + range: [21, 26] + loc: + start: + line: 5 + column: 0 + end: + line: 5 + column: 5 + start: 16 + end: 26 + range: [16, 26] + loc: + start: + line: 4 + column: 2 + end: + line: 5 + column: 5 + ] + start: 14 + end: 26 + range: [14, 26] + loc: + start: + line: 4 + column: 0 + end: + line: 5 + column: 5 + start: 0 + end: 26 + range: [0, 26] + loc: + start: + line: 1 + column: 0 + end: + line: 5 + column: 5 + + testAstLocationData ''' + a = + if b then c else if d then e + ''', + type: 'AssignmentExpression' + right: + test: + start: 9 + end: 10 + range: [9, 10] + loc: + start: + line: 2 + column: 5 + end: + line: 2 + column: 6 + consequent: + start: 16 + end: 17 + range: [16, 17] + loc: + start: + line: 2 + column: 12 + end: + line: 2 + column: 13 + alternate: + test: + start: 26 + end: 27 + range: [26, 27] + loc: + start: + line: 2 + column: 22 + end: + line: 2 + column: 23 + consequent: + start: 33 + end: 34 + range: [33, 34] + loc: + start: + line: 2 + column: 29 + end: + line: 2 + column: 30 + start: 23 + end: 34 + range: [23, 34] + loc: + start: + line: 2 + column: 19 + end: + line: 2 + column: 30 + start: 6 + end: 34 + range: [6, 34] + loc: + start: + line: 2 + column: 2 + end: + line: 2 + column: 30 + start: 0 + end: 34 + range: [0, 34] + loc: + start: + line: 1 + column: 0 + end: + line: 2 + column: 30 + + testAstLocationData ''' + f( + if b + c + d + ) + ''', + type: 'CallExpression' + arguments: [ + test: + start: 8 + end: 9 + range: [8, 9] + loc: + start: + line: 2 + column: 5 + end: + line: 2 + column: 6 + consequent: + body: [ + expression: + start: 14 + end: 15 + range: [14, 15] + loc: + start: + line: 3 + column: 4 + end: + line: 3 + column: 5 + start: 14 + end: 15 + range: [14, 15] + loc: + start: + line: 3 + column: 4 + end: + line: 3 + column: 5 + , + expression: + start: 20 + end: 21 + range: [20, 21] + loc: + start: + line: 4 + column: 4 + end: + line: 4 + column: 5 + start: 20 + end: 21 + range: [20, 21] + loc: + start: + line: 4 + column: 4 + end: + line: 4 + column: 5 + ] + start: 10 + end: 21 + range: [10, 21] + loc: + start: + line: 3 + column: 0 + end: + line: 4 + column: 5 + ] + start: 0 + end: 23 + range: [0, 23] + loc: + start: + line: 1 + column: 0 + end: + line: 5 + column: 1