From 858ba71201ed697e5c008c0a3bed38a919fa91d4 Mon Sep 17 00:00:00 2001 From: Julian Rosse Date: Fri, 26 Apr 2019 05:27:22 -0400 Subject: [PATCH 1/3] class prototype property AST --- lib/coffeescript/nodes.js | 51 +++++++++- src/nodes.coffee | 33 ++++++- test/abstract_syntax_tree.coffee | 22 +++++ .../abstract_syntax_tree_location_data.coffee | 95 +++++++++++++++++++ test/classes.coffee | 8 ++ 5 files changed, 206 insertions(+), 3 deletions(-) diff --git a/lib/coffeescript/nodes.js b/lib/coffeescript/nodes.js index 11b9c0ef27..c4b7e5ff7f 100644 --- a/lib/coffeescript/nodes.js +++ b/lib/coffeescript/nodes.js @@ -4,7 +4,7 @@ // nodes are created as the result of actions in the [grammar](grammar.html), // but some are created by other nodes as a method of code generation. To convert // the syntax tree into a string of JavaScript code, call `compile()` on the root. - var Access, Arr, Assign, AwaitReturn, Base, Block, BooleanLiteral, Call, Catch, Class, ClassProperty, Code, CodeFragment, ComputedPropertyName, DefaultLiteral, DynamicImport, DynamicImportCall, Elision, ExecutableClassBody, Existence, Expansion, ExportAllDeclaration, ExportDeclaration, ExportDefaultDeclaration, ExportNamedDeclaration, ExportSpecifier, ExportSpecifierList, Extends, For, FuncDirectiveReturn, FuncGlyph, HEREGEX_OMIT, HereComment, HoistTarget, IdentifierLiteral, If, ImportClause, ImportDeclaration, ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier, ImportSpecifierList, In, Index, InfinityLiteral, Interpolation, JSXAttribute, JSXAttributes, JSXElement, JSXEmptyExpression, JSXExpressionContainer, JSXIdentifier, JSXTag, JSXText, JS_FORBIDDEN, LEADING_BLANK_LINE, LEVEL_ACCESS, LEVEL_COND, LEVEL_LIST, LEVEL_OP, LEVEL_PAREN, LEVEL_TOP, LineComment, Literal, MetaProperty, ModuleDeclaration, ModuleSpecifier, ModuleSpecifierList, NEGATE, NO, NaNLiteral, NullLiteral, NumberLiteral, Obj, ObjectProperty, Op, Param, Parens, PassthroughLiteral, PropertyName, Range, RegexLiteral, RegexWithInterpolations, Return, Root, SIMPLENUM, SIMPLE_STRING_OMIT, STRING_OMIT, Scope, Slice, Splat, StatementLiteral, StringLiteral, StringWithInterpolations, Super, SuperCall, Switch, SwitchCase, SwitchWhen, TAB, THIS, TRAILING_BLANK_LINE, TaggedTemplateCall, TemplateElement, ThisLiteral, Throw, Try, UTILITIES, UndefinedLiteral, Value, While, YES, YieldReturn, addDataToNode, attachCommentsToNode, compact, del, ends, extend, flatten, fragmentsToText, greater, hasLineComments, indentInitial, isAstLocGreater, isFunction, isLiteralArguments, isLiteralThis, isLocationDataEndGreater, isLocationDataStartGreater, isNumber, isPlainObject, isUnassignable, jisonLocationDataToAstLocationData, lesser, locationDataToString, makeDelimitedLiteral, merge, mergeAstLocationData, mergeLocationData, moveComments, multident, replaceUnicodeCodePointEscapes, shouldCacheOrIsAssignable, some, starts, throwSyntaxError, unfoldSoak, unshiftAfterComments, utility, zeroWidthLocationDataFromEndLocation, + var Access, Arr, Assign, AwaitReturn, Base, Block, BooleanLiteral, Call, Catch, Class, ClassProperty, ClassPrototypeProperty, Code, CodeFragment, ComputedPropertyName, DefaultLiteral, DynamicImport, DynamicImportCall, Elision, ExecutableClassBody, Existence, Expansion, ExportAllDeclaration, ExportDeclaration, ExportDefaultDeclaration, ExportNamedDeclaration, ExportSpecifier, ExportSpecifierList, Extends, For, FuncDirectiveReturn, FuncGlyph, HEREGEX_OMIT, HereComment, HoistTarget, IdentifierLiteral, If, ImportClause, ImportDeclaration, ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier, ImportSpecifierList, In, Index, InfinityLiteral, Interpolation, JSXAttribute, JSXAttributes, JSXElement, JSXEmptyExpression, JSXExpressionContainer, JSXIdentifier, JSXTag, JSXText, JS_FORBIDDEN, LEADING_BLANK_LINE, LEVEL_ACCESS, LEVEL_COND, LEVEL_LIST, LEVEL_OP, LEVEL_PAREN, LEVEL_TOP, LineComment, Literal, MetaProperty, ModuleDeclaration, ModuleSpecifier, ModuleSpecifierList, NEGATE, NO, NaNLiteral, NullLiteral, NumberLiteral, Obj, ObjectProperty, Op, Param, Parens, PassthroughLiteral, PropertyName, Range, RegexLiteral, RegexWithInterpolations, Return, Root, SIMPLENUM, SIMPLE_STRING_OMIT, STRING_OMIT, Scope, Slice, Splat, StatementLiteral, StringLiteral, StringWithInterpolations, Super, SuperCall, Switch, SwitchCase, SwitchWhen, TAB, THIS, TRAILING_BLANK_LINE, TaggedTemplateCall, TemplateElement, ThisLiteral, Throw, Try, UTILITIES, UndefinedLiteral, Value, While, YES, YieldReturn, addDataToNode, attachCommentsToNode, compact, del, ends, extend, flatten, fragmentsToText, greater, hasLineComments, indentInitial, isAstLocGreater, isFunction, isLiteralArguments, isLiteralThis, isLocationDataEndGreater, isLocationDataStartGreater, isNumber, isPlainObject, isUnassignable, jisonLocationDataToAstLocationData, lesser, locationDataToString, makeDelimitedLiteral, merge, mergeAstLocationData, mergeLocationData, moveComments, multident, replaceUnicodeCodePointEscapes, shouldCacheOrIsAssignable, some, starts, throwSyntaxError, unfoldSoak, unshiftAfterComments, utility, zeroWidthLocationDataFromEndLocation, indexOf = [].indexOf, splice = [].splice, slice1 = [].slice; @@ -4017,6 +4017,8 @@ return this.addInitializerMethod(node); } else if (!o.compiling && this.validClassProperty(node)) { return this.addClassProperty(node); + } else if (!o.compiling && this.validClassInstanceProperty(node)) { + return this.addClassInstanceProperty(node); } else { return null; } @@ -4080,6 +4082,22 @@ }).withLocationDataFrom(assign); } + validClassInstanceProperty(node) { + if (!(node instanceof Assign)) { + return false; + } + return node.context === 'object' && !node.variable.hasProperties(); + } + + addClassInstanceProperty(assign) { + var value, variable; + ({variable, value} = assign); + return new ClassPrototypeProperty({ + name: variable.base, + value + }).withLocationDataFrom(assign); + } + makeDefaultConstructor() { var applyArgs, applyCtor, ctor; ctor = this.addInitializerMethod(new Assign(new Value(new PropertyName('constructor')), new Code())); @@ -4274,7 +4292,7 @@ // The class scope is not available yet, so return the assignment to update later assign = this.externalCtor = new Assign(new Value(), value); } else if (!assign.variable.this) { - name = new (base.shouldCache() ? Index : Access)(base); + name = base instanceof ComputedPropertyName ? new Index(base.value) : new (base.shouldCache() ? Index : Access)(base); prototype = new Access(new PropertyName('prototype')); variable = new Value(new ThisLiteral(), [prototype, name]); assign.variable = variable; @@ -4337,6 +4355,35 @@ }).call(this); + exports.ClassPrototypeProperty = ClassPrototypeProperty = (function() { + class ClassPrototypeProperty extends Base { + constructor({ + name: name1, + value: value1 + }) { + super(); + this.name = name1; + this.value = value1; + } + + astProperties(o) { + return { + key: this.name.ast(o, LEVEL_LIST), + value: this.value.ast(o, LEVEL_LIST), + computed: this.name instanceof ComputedPropertyName + }; + } + + }; + + ClassPrototypeProperty.prototype.children = ['name', 'value']; + + ClassPrototypeProperty.prototype.isStatement = YES; + + return ClassPrototypeProperty; + + }).call(this); + //### Import and Export exports.ModuleDeclaration = ModuleDeclaration = (function() { class ModuleDeclaration extends Base { diff --git a/src/nodes.coffee b/src/nodes.coffee index b596c024a0..d010192b14 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -2694,6 +2694,8 @@ exports.Class = class Class extends Base @addInitializerMethod node else if not o.compiling and @validClassProperty node @addClassProperty node + else if not o.compiling and @validClassInstanceProperty node + @addClassInstanceProperty node else null @@ -2736,6 +2738,17 @@ exports.Class = class Class extends Base operatorToken }).withLocationDataFrom assign + validClassInstanceProperty: (node) -> + return no unless node instanceof Assign + node.context is 'object' and not node.variable.hasProperties() + + addClassInstanceProperty: (assign) -> + {variable, value} = assign + new ClassPrototypeProperty({ + name: variable.base + value + }).withLocationDataFrom assign + makeDefaultConstructor: -> ctor = @addInitializerMethod new Assign (new Value new PropertyName 'constructor'), new Code @body.unshift ctor @@ -2883,7 +2896,11 @@ exports.ExecutableClassBody = class ExecutableClassBody extends Base # The class scope is not available yet, so return the assignment to update later assign = @externalCtor = new Assign new Value, value else if not assign.variable.this - name = new (if base.shouldCache() then Index else Access) base + name = + if base instanceof ComputedPropertyName + new Index base.value + else + new (if base.shouldCache() then Index else Access) base prototype = new Access new PropertyName 'prototype' variable = new Value new ThisLiteral(), [ prototype, name ] @@ -2911,6 +2928,20 @@ exports.ClassProperty = class ClassProperty extends Base operator: @operatorToken?.value ? '=' staticClassName: @staticClassName?.ast(o) ? null +exports.ClassPrototypeProperty = class ClassPrototypeProperty extends Base + constructor: ({@name, @value}) -> + super() + + children: ['name', 'value'] + + isStatement: YES + + astProperties: (o) -> + return + key: @name.ast o, LEVEL_LIST + value: @value.ast o, LEVEL_LIST + computed: @name instanceof ComputedPropertyName + #### Import and Export exports.ModuleDeclaration = class ModuleDeclaration extends Base diff --git a/test/abstract_syntax_tree.coffee b/test/abstract_syntax_tree.coffee index 1447231480..c4b8d75fbd 100644 --- a/test/abstract_syntax_tree.coffee +++ b/test/abstract_syntax_tree.coffee @@ -1475,6 +1475,28 @@ test "AST as expected for Class node", -> shorthand: no ] + testStatement ''' + class A + b: 1 + [c]: 2 + ''', + type: 'ClassDeclaration' + id: ID 'A' + superClass: null + body: + type: 'ClassBody' + body: [ + type: 'ClassPrototypeProperty' + key: ID 'b' + value: NUMBER 1 + computed: no + , + type: 'ClassPrototypeProperty' + key: ID 'c' + value: NUMBER 2 + computed: yes + ] + # test "AST as expected for ExecutableClassBody node", -> # code = """ # class Klass diff --git a/test/abstract_syntax_tree_location_data.coffee b/test/abstract_syntax_tree_location_data.coffee index 8dc29d724f..b27180f6b0 100644 --- a/test/abstract_syntax_tree_location_data.coffee +++ b/test/abstract_syntax_tree_location_data.coffee @@ -7010,3 +7010,98 @@ test "AST as expected for Class node", -> end: line: 9 column: 12 + + testAstLocationData ''' + class A + b: 1 + [c]: 2 + ''', + type: 'ClassDeclaration' + body: + body: [ + key: + start: 10 + end: 11 + range: [10, 11] + loc: + start: + line: 2 + column: 2 + end: + line: 2 + column: 3 + value: + start: 13 + end: 14 + range: [13, 14] + loc: + start: + line: 2 + column: 5 + end: + line: 2 + column: 6 + start: 10 + end: 14 + range: [10, 14] + loc: + start: + line: 2 + column: 2 + end: + line: 2 + column: 6 + , + key: + start: 18 + end: 19 + range: [18, 19] + loc: + start: + line: 3 + column: 3 + end: + line: 3 + column: 4 + value: + start: 22 + end: 23 + range: [22, 23] + loc: + start: + line: 3 + column: 7 + end: + line: 3 + column: 8 + start: 17 + end: 23 + range: [17, 23] + loc: + start: + line: 3 + column: 2 + end: + line: 3 + column: 8 + ] + start: 8 + end: 23 + range: [8, 23] + loc: + start: + line: 2 + column: 0 + end: + line: 3 + column: 8 + start: 0 + end: 23 + range: [0, 23] + loc: + start: + line: 1 + column: 0 + end: + line: 3 + column: 8 diff --git a/test/classes.coffee b/test/classes.coffee index 442a379862..28777070a4 100644 --- a/test/classes.coffee +++ b/test/classes.coffee @@ -1930,3 +1930,11 @@ test "#5085: Bug: @ reference to class not maintained in do block", -> eq thisFoo, 'foo assigned in class' eq thisBar, 'foo assigned in class' + +test "#5204: Computed class property", -> + foo = 'bar' + class A + [foo]: 'baz' + a = new A() + eq a.bar, 'baz' + eq A::bar, 'baz' From 76699e575c95bcf4448d606d5156cdf6da603876 Mon Sep 17 00:00:00 2001 From: Julian Rosse Date: Fri, 26 Apr 2019 05:35:01 -0400 Subject: [PATCH 2/3] consistent naming --- lib/coffeescript/nodes.js | 8 ++++---- src/nodes.coffee | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/coffeescript/nodes.js b/lib/coffeescript/nodes.js index c4b7e5ff7f..7a0d920d72 100644 --- a/lib/coffeescript/nodes.js +++ b/lib/coffeescript/nodes.js @@ -4017,8 +4017,8 @@ return this.addInitializerMethod(node); } else if (!o.compiling && this.validClassProperty(node)) { return this.addClassProperty(node); - } else if (!o.compiling && this.validClassInstanceProperty(node)) { - return this.addClassInstanceProperty(node); + } else if (!o.compiling && this.validClassPrototypeProperty(node)) { + return this.addClassPrototypeProperty(node); } else { return null; } @@ -4082,14 +4082,14 @@ }).withLocationDataFrom(assign); } - validClassInstanceProperty(node) { + validClassPrototypeProperty(node) { if (!(node instanceof Assign)) { return false; } return node.context === 'object' && !node.variable.hasProperties(); } - addClassInstanceProperty(assign) { + addClassPrototypeProperty(assign) { var value, variable; ({variable, value} = assign); return new ClassPrototypeProperty({ diff --git a/src/nodes.coffee b/src/nodes.coffee index d010192b14..00a536c915 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -2694,8 +2694,8 @@ exports.Class = class Class extends Base @addInitializerMethod node else if not o.compiling and @validClassProperty node @addClassProperty node - else if not o.compiling and @validClassInstanceProperty node - @addClassInstanceProperty node + else if not o.compiling and @validClassPrototypeProperty node + @addClassPrototypeProperty node else null @@ -2738,11 +2738,11 @@ exports.Class = class Class extends Base operatorToken }).withLocationDataFrom assign - validClassInstanceProperty: (node) -> + validClassPrototypeProperty: (node) -> return no unless node instanceof Assign node.context is 'object' and not node.variable.hasProperties() - addClassInstanceProperty: (assign) -> + addClassPrototypeProperty: (assign) -> {variable, value} = assign new ClassPrototypeProperty({ name: variable.base From 1650675d21abad591d047d75cfdcf07a791edee6 Mon Sep 17 00:00:00 2001 From: Julian Rosse Date: Sun, 28 Apr 2019 11:58:36 -0400 Subject: [PATCH 3/3] extract fix for #5204 --- lib/coffeescript/nodes.js | 2 +- src/nodes.coffee | 6 +----- test/classes.coffee | 8 -------- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/lib/coffeescript/nodes.js b/lib/coffeescript/nodes.js index 7a0d920d72..af18f89a41 100644 --- a/lib/coffeescript/nodes.js +++ b/lib/coffeescript/nodes.js @@ -4292,7 +4292,7 @@ // The class scope is not available yet, so return the assignment to update later assign = this.externalCtor = new Assign(new Value(), value); } else if (!assign.variable.this) { - name = base instanceof ComputedPropertyName ? new Index(base.value) : new (base.shouldCache() ? Index : Access)(base); + name = new (base.shouldCache() ? Index : Access)(base); prototype = new Access(new PropertyName('prototype')); variable = new Value(new ThisLiteral(), [prototype, name]); assign.variable = variable; diff --git a/src/nodes.coffee b/src/nodes.coffee index 00a536c915..c08ad4647a 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -2896,11 +2896,7 @@ exports.ExecutableClassBody = class ExecutableClassBody extends Base # The class scope is not available yet, so return the assignment to update later assign = @externalCtor = new Assign new Value, value else if not assign.variable.this - name = - if base instanceof ComputedPropertyName - new Index base.value - else - new (if base.shouldCache() then Index else Access) base + name = new (if base.shouldCache() then Index else Access) base prototype = new Access new PropertyName 'prototype' variable = new Value new ThisLiteral(), [ prototype, name ] diff --git a/test/classes.coffee b/test/classes.coffee index 28777070a4..442a379862 100644 --- a/test/classes.coffee +++ b/test/classes.coffee @@ -1930,11 +1930,3 @@ test "#5085: Bug: @ reference to class not maintained in do block", -> eq thisFoo, 'foo assigned in class' eq thisBar, 'foo assigned in class' - -test "#5204: Computed class property", -> - foo = 'bar' - class A - [foo]: 'baz' - a = new A() - eq a.bar, 'baz' - eq A::bar, 'baz'