Merge pull request #15 from GeoffreyBooth/value-ast-revisions-helpers

AST helpers revisions
This commit is contained in:
Julian Rosse
2018-09-25 22:04:54 -04:00
committed by GitHub
12 changed files with 360 additions and 352 deletions

View File

@@ -279,10 +279,7 @@ buildDocTests = (watch = no) ->
outputFolder = "docs/v#{majorVersion}"
# Included in test.html
readHelpersFile = (basename) ->
fs.readFileSync("test/support/#{basename}.coffee", 'utf-8').replace /exports\./g, '@'
testHelpers = readHelpersFile('helpers') + '\n'
testHelpers += readHelpersFile('abstract_syntax_tree_helpers')
testHelpers = fs.readFileSync('test/support/helpers.coffee', 'utf-8').replace /exports\./g, '@'
# Helpers
testsInScriptBlocks = ->
@@ -421,7 +418,6 @@ runTests = (CoffeeScript) ->
onFail description, fn, err
helpers.extend global, require './test/support/helpers'
helpers.extend global, require './test/support/abstract_syntax_tree_helpers'
# When all the tests have run, collect and print errors.
# If a stacktrace is available, output the compiled function source.

View File

@@ -48,7 +48,7 @@
// is added to the first parameter passed in, and the parameter is returned.
// If the parameter is not a node, it will just be passed through unaffected.
getAddDataToNodeFunctionString = function(first, last) {
return `yy.addDataToNode(yy, {first: @${first}, ${last ? `last: @${last}, ` : ''}})`;
return `yy.addDataToNode(yy, @${first}${last ? `, @${last}` : ''})`;
};
action = action.replace(/LOC\(([0-9]*)\)/g, getAddDataToNodeFunctionString('$1'));
action = action.replace(/LOC\(([0-9]*),\s*([0-9]*)\)/g, getAddDataToNodeFunctionString('$1', '$2'));

View File

@@ -199,14 +199,12 @@
// This returns a function which takes an object as a parameter, and if that
// object is an AST node, updates that object's locationData.
// The object is returned either way.
exports.addDataToNode = function(parserState, {first, last, forceUpdateLocation = true}) {
exports.addDataToNode = function(parserState, first, last, forceUpdateLocation = true) {
return function(obj) {
var objHash, ref1;
// Add location data.
if (((obj != null ? obj.updateLocationDataIfMissing : void 0) != null) && (first != null)) {
obj.updateLocationDataIfMissing(buildLocationData(first, last), {
force: forceUpdateLocation
});
obj.updateLocationDataIfMissing(buildLocationData(first, last), forceUpdateLocation);
}
// Add comments, building the dictionary of token data if it hasnt been
// built yet.

View File

@@ -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, CSXTag, Call, Class, Code, CodeFragment, ComputedPropertyName, Elision, ExecutableClassBody, Existence, Expansion, ExportAllDeclaration, ExportDeclaration, ExportDefaultDeclaration, ExportNamedDeclaration, ExportSpecifier, ExportSpecifierList, Extends, For, FuncGlyph, HEREGEX_OMIT, HereComment, HoistTarget, IdentifierLiteral, If, ImportClause, ImportDeclaration, ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier, ImportSpecifierList, In, Index, InfinityLiteral, Interpolation, JS_FORBIDDEN, LEADING_BLANK_LINE, LEVEL_ACCESS, LEVEL_COND, LEVEL_LIST, LEVEL_OP, LEVEL_PAREN, LEVEL_TOP, LineComment, Literal, ModuleDeclaration, ModuleSpecifier, ModuleSpecifierList, NEGATE, NO, NaNLiteral, NullLiteral, NumberLiteral, Obj, Op, Param, Parens, PassthroughLiteral, PropertyName, Range, RegexLiteral, RegexWithInterpolations, Return, SIMPLENUM, SIMPLE_STRING_OMIT, STRING_OMIT, Scope, Slice, Splat, StatementLiteral, StringLiteral, StringWithInterpolations, Super, SuperCall, Switch, TAB, THIS, TRAILING_BLANK_LINE, TaggedTemplateCall, ThisLiteral, Throw, Try, UTILITIES, UndefinedLiteral, Value, While, YES, YieldReturn, addDataToNode, attachCommentsToNode, compact, del, ends, extend, flatten, fragmentsToText, hasLineComments, indentInitial, isFunction, isLiteralArguments, isLiteralThis, isNumber, isPlainObject, isUnassignable, locationDataToString, makeDelimitedLiteral, merge, mergeAstLocationData, moveComments, multident, replaceUnicodeCodePointEscapes, shouldCacheOrIsAssignable, some, starts, throwSyntaxError, unfoldSoak, unshiftAfterComments, utility,
var Access, Arr, Assign, AwaitReturn, Base, Block, BooleanLiteral, CSXTag, Call, Class, Code, CodeFragment, ComputedPropertyName, Elision, ExecutableClassBody, Existence, Expansion, ExportAllDeclaration, ExportDeclaration, ExportDefaultDeclaration, ExportNamedDeclaration, ExportSpecifier, ExportSpecifierList, Extends, For, FuncGlyph, HEREGEX_OMIT, HereComment, HoistTarget, IdentifierLiteral, If, ImportClause, ImportDeclaration, ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier, ImportSpecifierList, In, Index, InfinityLiteral, Interpolation, JS_FORBIDDEN, LEADING_BLANK_LINE, LEVEL_ACCESS, LEVEL_COND, LEVEL_LIST, LEVEL_OP, LEVEL_PAREN, LEVEL_TOP, LineComment, Literal, ModuleDeclaration, ModuleSpecifier, ModuleSpecifierList, NEGATE, NO, NaNLiteral, NullLiteral, NumberLiteral, Obj, Op, Param, Parens, PassthroughLiteral, PropertyName, Range, RegexLiteral, RegexWithInterpolations, Return, SIMPLENUM, SIMPLE_STRING_OMIT, STRING_OMIT, Scope, Slice, Splat, StatementLiteral, StringLiteral, StringWithInterpolations, Super, SuperCall, Switch, TAB, THIS, TRAILING_BLANK_LINE, TaggedTemplateCall, ThisLiteral, Throw, Try, UTILITIES, UndefinedLiteral, Value, While, YES, YieldReturn, addDataToNode, attachCommentsToNode, compact, del, ends, extend, flatten, fragmentsToText, greater, hasLineComments, indentInitial, isFunction, isLiteralArguments, isLiteralThis, isNumber, isPlainObject, isUnassignable, lesser, locationDataToString, makeDelimitedLiteral, merge, mergeAstLocationData, moveComments, multident, replaceUnicodeCodePointEscapes, shouldCacheOrIsAssignable, some, starts, throwSyntaxError, unfoldSoak, unshiftAfterComments, utility,
indexOf = [].indexOf,
splice = [].splice,
slice1 = [].slice;
@@ -491,7 +491,7 @@
// For this node and all descendents, set the location data to `locationData`
// if the location data is not already set.
updateLocationDataIfMissing(locationData, {force} = {}) {
updateLocationDataIfMissing(locationData, force) {
if (force) {
this.forceUpdateLocation = true;
}
@@ -1746,17 +1746,18 @@
ref1 = this.properties;
for (propIndex = j = 0, len1 = ref1.length; j < len1; propIndex = ++j) {
prop = ref1[propIndex];
ret = mergeAstLocationData(Object.assign({
ret = {
type: 'MemberExpression',
object: ret,
property: prop.ast(),
computed: prop instanceof Index || !(((ref2 = prop.name) != null ? ref2.unwrap() : void 0) instanceof PropertyName),
optional: !!prop.soak,
shorthand: !!prop.shorthand
}, prop.astLocationData()), ret);
if (propIndex === 0 && this.base instanceof Parens && (this.base.locationData != null)) {
mergeAstLocationData(ret, this.base.astLocationData());
}
};
// When the `Value` has properties, the location data of a `MemberExpression` AST node
// corresponding to a given property should span the location of the `Value`'s `base`
// (including parens if present) through the property's location
Object.assign(ret, mergeAstLocationData(this.base.astLocationData(), prop.astLocationData()));
}
return ret;
}
@@ -6298,36 +6299,42 @@
return `${options.delimiter}${body}${options.delimiter}`;
};
// Extends the location data of an AST node to include the location data from
// another AST node.
mergeAstLocationData = function(intoNode, fromNode) {
var fromRange, intoRange;
({
range: intoRange
} = intoNode);
({
range: fromRange
} = fromNode);
if (!(intoRange && fromRange)) {
return intoNode;
// Take two AST nodes, or two AST nodes location data objects, and return a new
// location data object that encompasses the location data of both nodes. So the
// new `start` value will be the earlier of the two nodes `start` values, the
// new `end` value will be the later of the two nodes `end` values, etc.
lesser = function(a, b) {
if (a < b) {
return a;
} else {
return b;
}
if (fromRange[0] < intoRange[0]) {
intoNode.range = intoRange = [fromRange[0], intoRange[1]];
intoNode.start = fromNode.start;
intoNode.loc = {
start: fromNode.loc.start,
end: intoNode.loc.end
};
};
greater = function(a, b) {
if (a > b) {
return a;
} else {
return b;
}
if (fromRange[1] > intoRange[1]) {
intoNode.range = [intoRange[0], fromRange[1]];
intoNode.end = fromNode.end;
intoNode.loc = {
start: intoNode.loc.start,
end: fromNode.loc.end
};
}
return intoNode;
};
mergeAstLocationData = function(nodeA, nodeB) {
return {
loc: {
start: {
line: lesser(nodeA.loc.start.line, nodeB.loc.start.line),
column: lesser(nodeA.loc.start.column, nodeB.loc.start.column)
},
end: {
line: greater(nodeA.loc.end.line, nodeB.loc.end.line),
column: greater(nodeA.loc.end.column, nodeB.loc.end.column)
}
},
range: [lesser(nodeA.range[0], nodeB.range[0]), greater(nodeA.range[1], nodeB.range[1])],
start: lesser(nodeA.start, nodeB.start),
end: greater(nodeA.end, nodeB.end)
};
};
}).call(this);

File diff suppressed because it is too large Load Diff

View File

@@ -45,7 +45,7 @@ o = (patternString, action, options) ->
# is added to the first parameter passed in, and the parameter is returned.
# If the parameter is not a node, it will just be passed through unaffected.
getAddDataToNodeFunctionString = (first, last) ->
"yy.addDataToNode(yy, {first: @#{first}, #{if last then "last: @#{last}, " else ''}})"
"yy.addDataToNode(yy, @#{first}#{if last then ", @#{last}" else ''})"
action = action.replace /LOC\(([0-9]*)\)/g, getAddDataToNodeFunctionString('$1')
action = action.replace /LOC\(([0-9]*),\s*([0-9]*)\)/g, getAddDataToNodeFunctionString('$1', '$2')

View File

@@ -136,11 +136,11 @@ buildTokenDataDictionary = (parserState) ->
# This returns a function which takes an object as a parameter, and if that
# object is an AST node, updates that object's locationData.
# The object is returned either way.
exports.addDataToNode = (parserState, {first, last, forceUpdateLocation = yes}) ->
exports.addDataToNode = (parserState, first, last, forceUpdateLocation = yes) ->
(obj) ->
# Add location data.
if obj?.updateLocationDataIfMissing? and first?
obj.updateLocationDataIfMissing buildLocationData(first, last), force: forceUpdateLocation
obj.updateLocationDataIfMissing buildLocationData(first, last), forceUpdateLocation
# Add comments, building the dictionary of token data if it hasnt been
# built yet.

View File

@@ -294,17 +294,17 @@ exports.Base = class Base
return
loc:
start:
line: first_line + 1
line: first_line + 1
column: first_column
end:
line: last_line + 1
line: last_line + 1
column: last_column + 1
range: [
range[0]
range[1]
]
start: range[0]
end: range[1]
end: range[1]
# Passes each child to a function, breaking when the function returns `false`.
eachChild: (func) ->
@@ -398,7 +398,7 @@ exports.Base = class Base
# For this node and all descendents, set the location data to `locationData`
# if the location data is not already set.
updateLocationDataIfMissing: (locationData, {force} = {}) ->
updateLocationDataIfMissing: (locationData, force) ->
@forceUpdateLocation = yes if force
return this if @locationData and not @forceUpdateLocation
delete @forceUpdateLocation
@@ -1154,19 +1154,16 @@ exports.Value = class Value extends Base
ret = @base.ast()
for prop, propIndex in @properties
ret =
mergeAstLocationData(
Object.assign {
type: 'MemberExpression'
object: ret
property: prop.ast()
computed: prop instanceof Index or prop.name?.unwrap() not instanceof PropertyName
optional: !!prop.soak
shorthand: !!prop.shorthand
}, prop.astLocationData()
ret
)
if propIndex is 0 and @base instanceof Parens and @base.locationData?
mergeAstLocationData ret, @base.astLocationData()
type: 'MemberExpression'
object: ret
property: prop.ast()
computed: prop instanceof Index or prop.name?.unwrap() not instanceof PropertyName
optional: !!prop.soak
shorthand: !!prop.shorthand
# When the `Value` has properties, the location data of a `MemberExpression` AST node
# corresponding to a given property should span the location of the `Value`'s `base`
# (including parens if present) through the property's location
Object.assign ret, mergeAstLocationData(@base.astLocationData(), prop.astLocationData())
ret
checkNewTarget: (o) ->
@@ -4166,28 +4163,24 @@ makeDelimitedLiteral = (body, options = {}) ->
when other then (if options.double then "\\#{other}" else other)
"#{options.delimiter}#{body}#{options.delimiter}"
# Extends the location data of an AST node to include the location data from
# another AST node.
mergeAstLocationData = (intoNode, fromNode) ->
{range: intoRange} = intoNode
{range: fromRange} = fromNode
return intoNode unless intoRange and fromRange
if fromRange[0] < intoRange[0]
intoNode.range = intoRange = [
fromRange[0]
intoRange[1]
# Take two AST nodes, or two AST nodes location data objects, and return a new
# location data object that encompasses the location data of both nodes. So the
# new `start` value will be the earlier of the two nodes `start` values, the
# new `end` value will be the later of the two nodes `end` values, etc.
lesser = (a, b) -> if a < b then a else b
greater = (a, b) -> if a > b then a else b
mergeAstLocationData = (nodeA, nodeB) ->
return
loc:
start:
line: lesser nodeA.loc.start.line, nodeB.loc.start.line
column: lesser nodeA.loc.start.column, nodeB.loc.start.column
end:
line: greater nodeA.loc.end.line, nodeB.loc.end.line
column: greater nodeA.loc.end.column, nodeB.loc.end.column
range: [
lesser nodeA.range[0], nodeB.range[0]
greater nodeA.range[1], nodeB.range[1]
]
intoNode.start = fromNode.start
intoNode.loc =
start: fromNode.loc.start
end: intoNode.loc.end
if fromRange[1] > intoRange[1]
intoNode.range = [
intoRange[0]
fromRange[1]
]
intoNode.end = fromNode.end
intoNode.loc =
start: intoNode.loc.start
end: fromNode.loc.end
intoNode
start: lesser nodeA.start, nodeB.start
end: greater nodeA.end, nodeB.end

View File

@@ -4,37 +4,41 @@
# Recursively compare all values of enumerable properties of `expected` with
# those of `actual`. Use `looseArray` helper function to skip array length
# comparison.
deepStrictEqualExpectedProperties = (actual, expected) ->
white = (text, values...) -> (text[i] + "#{reset}#{value}#{red}" for value, i in values).join('') + text[i]
deepStrictIncludeExpectedProperties = (actual, expected) ->
eq actual.length, expected.length if expected instanceof Array and not expected.loose
for key, val of expected
if 'object' is typeof val
fail white"Property #{key} expected, but was missing" unless actual[key]
deepStrictEqualExpectedProperties actual[key], val
fail "Property #{reset}#{key}#{red} expected, but was missing" unless actual[key]
deepStrictIncludeExpectedProperties actual[key], val
else
eq actual[key], val, white"Property #{key}: expected #{actual[key]} to equal #{val}"
eq actual[key], val, """
Property #{reset}#{key}#{red}: expected #{reset}#{actual[key]}#{red} to equal #{reset}#{val}#{red}
Expected AST output to include:
#{reset}#{inspect expected}#{red}
but instead it was:
#{reset}#{inspect actual}#{red}
"""
actual
testExpression = (code, expected) ->
ast = getExpressionAst code
if expected?
deepStrictEqualExpectedProperties ast, expected
else
# Convenience for creating new tests; call `testExpression` with no second
# parameter to see what the current AST generation is for your input code.
console.log require('util').inspect ast,
depth: 10
colors: yes
# Flag array for loose comparision. See reference to `.loose` in
# `deepStrictEqualExpectedProperties` above.
# Flag array for loose comparison. See reference to `.loose` in
# `deepStrictIncludeExpectedProperties` above.
looseArray = (arr) ->
Object.defineProperty arr, 'loose',
value: yes
enumerable: no
arr
test 'Confirm functionality of `deepStrictEqualExpectedProperties`', ->
testExpression = (code, expected) ->
ast = getAstExpression code
if expected?
deepStrictIncludeExpectedProperties ast, expected
else
# Convenience for creating new tests; call `testExpression` with no second
# parameter to see what the current AST generation is for your input code.
console.log inspect ast
test 'Confirm functionality of `deepStrictIncludeExpectedProperties`', ->
actual =
name: 'Name'
a:
@@ -43,7 +47,7 @@ test 'Confirm functionality of `deepStrictEqualExpectedProperties`', ->
x: [1, 2, 3]
check = (message, test, expected) ->
test (-> deepStrictEqualExpectedProperties actual, expected), message
test (-> deepStrictIncludeExpectedProperties actual, expected), message
check 'Expected property does not match', throws,
name: '"Name"'
@@ -88,7 +92,7 @@ test 'Confirm functionality of `deepStrictEqualExpectedProperties`', ->
# properties are as expected.
test "AST as expected for Block node", ->
deepStrictEqualExpectedProperties CoffeeScript.compile('return', ast: yes),
deepStrictIncludeExpectedProperties CoffeeScript.compile('return', ast: yes),
type: 'Block'
expressions: [
type: 'Return'

View File

@@ -2,7 +2,7 @@
# ---------------------------------
testAstLocationData = (code, expected) ->
testAstNodeLocationData getExpressionAst(code), expected
testAstNodeLocationData getAstExpression(code), expected
testAstNodeLocationData = (node, expected, path = '') ->
extendPath = (additionalPath) ->
@@ -16,10 +16,9 @@ testAstNodeLocationData = (node, expected, path = '') ->
for expectedItem, index in expectedChild
testAstNodeLocationData node[key][index], expectedItem, extendPath "#{key}[#{index}]"
else if typeof expectedChild is 'object'
testAstNodeLocationData node[key], expectedChild, extendPath key
testAstNodeLocationData node[key], expectedChild, extendPath(key)
testSingleNodeLocationData = (node, expected, path) ->
pathStr = if path then " at '#{path}'" else ''
testSingleNodeLocationData = (node, expected, path = '') ->
# Even though its not part of the location data, check the type to ensure
# that were testing the node we think we are.
if expected.type?
@@ -27,19 +26,20 @@ testSingleNodeLocationData = (node, expected, path) ->
"Expected AST node type #{reset}#{node.type}#{red} to equal #{reset}#{expected.type}#{red}"
eq node.start, expected.start, \
"Expected location start #{reset}#{node.start}#{red} to equal #{reset}#{expected.start}#{red}#{pathStr}"
"Expected #{path}.start: #{reset}#{node.start}#{red} to equal #{reset}#{expected.start}#{red}"
eq node.end, expected.end, \
"Expected location end #{reset}#{node.end}#{red} to equal #{reset}#{expected.end}#{red}#{pathStr}"
"Expected #{path}.end: #{reset}#{node.end}#{red} to equal #{reset}#{expected.end}#{red}"
arrayEq node.range, expected.range, \
"Expected location range #{reset}#{JSON.stringify node.range}#{red} to equal #{reset}#{JSON.stringify expected.range}#{red}#{pathStr}"
"Expected #{path}.range: #{reset}#{JSON.stringify node.range}#{red} to equal #{reset}#{JSON.stringify expected.range}#{red}"
eq node.loc.start.line, expected.loc.start.line, \
"Expected location start line #{reset}#{node.loc.start.line}#{red} to equal #{reset}#{expected.loc.start.line}#{red}#{pathStr}"
"Expected #{path}.loc.start.line: #{reset}#{node.loc.start.line}#{red} to equal #{reset}#{expected.loc.start.line}#{red}"
eq node.loc.start.column, expected.loc.start.column, \
"Expected location start column #{reset}#{node.loc.start.column}#{red} to equal #{reset}#{expected.loc.start.column}#{red}#{pathStr}"
"Expected #{path}.loc.start.column: #{reset}#{node.loc.start.column}#{red} to equal #{reset}#{expected.loc.start.column}#{red}"
eq node.loc.end.line, expected.loc.end.line, \
"Expected location end line #{reset}#{node.loc.end.line}#{red} to equal #{reset}#{expected.loc.end.line}#{red}#{pathStr}"
"Expected #{path}.loc.end.line: #{reset}#{node.loc.end.line}#{red} to equal #{reset}#{expected.loc.end.line}#{red}"
eq node.loc.end.column, expected.loc.end.column, \
"Expected location end column #{reset}#{node.loc.end.column}#{red} to equal #{reset}#{expected.loc.end.column}#{red}#{pathStr}"
"Expected #{path}.loc.end.column: #{reset}#{node.loc.end.column}#{red} to equal #{reset}#{expected.loc.end.column}#{red}"
test "AST location data as expected for NumberLiteral node", ->
testAstLocationData '42',

View File

@@ -1,8 +0,0 @@
# Helpers to get AST nodes for a string of code. The root node is always a
# `Block` node, so for brevity in the tests return its children from
# `expressions`.
getAstExpressions = (code) ->
ast = CoffeeScript.compile code, ast: yes
ast.expressions
exports.getExpressionAst = (code) -> getAstExpressions(code)[0]

View File

@@ -38,3 +38,21 @@ exports.eqJS = (input, expectedOutput, msg) ->
ok egal(expectedOutput, actualOutput), msg or diffOutput expectedOutput, actualOutput
exports.isWindows = -> process.platform is 'win32'
exports.inspect = (obj) ->
if global.testingBrowser
JSON.stringify obj, null, 2
else
require('util').inspect obj,
depth: 10
colors: if process.env.NODE_DISABLE_COLORS then no else yes
# Helpers to get AST nodes for a string of code. The root node is always a
# `Block` node, so for brevity in the tests return its children from
# `expressions`.
exports.getAstExpressions = (code) ->
ast = CoffeeScript.compile code, ast: yes
ast.expressions
# Many tests want just the root node.
exports.getAstExpression = (code) -> getAstExpressions(code)[0]