Skip to content

Commit fd0d180

Browse files
committed
Avoid free variable conflicts within descendant scopes instead of global
Instead of a global `referencedVars` option that is computed at the lexer level (via all IDENTIFIER tokens), recurse through the AST to mark each scope with all variables used within it and descendant scopes. Likely also more efficient via `Set`s instead of `Array`s. Also fix some missing `children` in `For` nodes, which otherwise caused this rewrite to fail. Fixes jashkenas#4865 by causing parameter renaming in fewer (more predictable) scenarios.
1 parent 6fd58ef commit fd0d180

File tree

8 files changed

+62
-50
lines changed

8 files changed

+62
-50
lines changed

lib/coffeescript/coffeescript.js

Lines changed: 1 addition & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/coffeescript/nodes.js

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

lib/coffeescript/repl.js

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

lib/coffeescript/scope.js

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

src/coffeescript.coffee

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,6 @@ exports.compile = compile = withPrettyErrors (code, options = {}) ->
9393

9494
tokens = lexer.tokenize code, options
9595

96-
# Pass a list of referenced variables, so that generated variables won’t get
97-
# the same name.
98-
options.referencedVars = (
99-
token[1] for token in tokens when token[0] is 'IDENTIFIER'
100-
)
101-
10296
# Check for import or export; if found, force bare mode.
10397
unless options.bare? and options.bare is yes
10498
for token in tokens
@@ -127,6 +121,7 @@ exports.compile = compile = withPrettyErrors (code, options = {}) ->
127121
ast.tokens = tokens
128122
return ast
129123

124+
nodes.findReferencedVars()
130125
fragments = nodes.compileToFragments options
131126

132127
currentLine = 0

src/nodes.coffee

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,10 @@ exports.Base = class Base
361361
else
362362
return true if children.replaceInContext match, replacement
363363

364+
findReferencedVars: (scopes) ->
365+
@eachChild (child) ->
366+
child.findReferencedVars scopes
367+
364368
invert: ->
365369
new Op '!', this
366370

@@ -515,9 +519,13 @@ exports.Root = class Root extends Base
515519
super()
516520

517521
@isAsync = (new Code [], @body).isAsync
522+
@referencedVars = new Set
518523

519524
children: ['body']
520525

526+
findReferencedVars: (scopes = []) ->
527+
super scopes.concat @
528+
521529
# Wrap everything in a safety closure, unless requested not to. It would be
522530
# better not to generate them in the first place, but for now, clean up
523531
# obvious double-parentheses.
@@ -532,7 +540,7 @@ exports.Root = class Root extends Base
532540
[].concat @makeCode("(#{functionKeyword}() {\n"), fragments, @makeCode("\n}).call(this);\n")
533541

534542
initializeScope: (o) ->
535-
o.scope = new Scope null, @body, null, o.referencedVars ? []
543+
o.scope = new Scope null, @body, null, @referencedVars
536544
# Mark given local variables in the root scope as parameters so they don’t
537545
# end up being declared on the root block.
538546
o.scope.parameter name for name in o.locals or []
@@ -1172,6 +1180,9 @@ exports.IdentifierLiteral = class IdentifierLiteral extends Literal
11721180
name: @value
11731181
declaration: !!@isDeclaration
11741182

1183+
findReferencedVars: (scopes) ->
1184+
scope.referencedVars.add @value for scope in scopes
1185+
11751186
exports.PropertyName = class PropertyName extends Literal
11761187
isAssignable: YES
11771188

@@ -3904,6 +3915,7 @@ exports.Code = class Code extends Base
39043915
@isGenerator = no
39053916
@isAsync = no
39063917
@isMethod = no
3918+
@referencedVars = new Set
39073919

39083920
@body.traverseChildren no, (node) =>
39093921
if (node instanceof Op and node.isYield()) or node instanceof YieldReturn
@@ -3921,7 +3933,11 @@ exports.Code = class Code extends Base
39213933

39223934
jumps: NO
39233935

3924-
makeScope: (parentScope) -> new Scope parentScope, @body, this
3936+
findReferencedVars: (scopes) ->
3937+
super scopes.concat @
3938+
3939+
makeScope: (parentScope) ->
3940+
new Scope parentScope, @body, this, @referencedVars
39253941

39263942
# Compilation creates a new scope unless explicitly asked to share with the
39273943
# outer scope. Handles splat parameters in the parameter list by setting
@@ -5342,7 +5358,7 @@ exports.For = class For extends While
53425358
@addBody body
53435359
@addSource source
53445360

5345-
children: ['body', 'source', 'guard', 'step']
5361+
children: ['body', 'name', 'index', 'source', 'guard', 'step']
53465362

53475363
isAwait: -> @await ? no
53485364

src/repl.coffee

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@ replDefaults =
3838
if tokens.length >= 1 and tokens[tokens.length - 1].generated and
3939
tokens[tokens.length - 1].comments?.length isnt 0 and "#{tokens[tokens.length - 1][1]}" is ''
4040
tokens.pop()
41-
# Collect referenced variable names just like in `CoffeeScript.compile`.
42-
referencedVars = (token[1] for token in tokens when token[0] is 'IDENTIFIER')
4341
# Generate the AST of the tokens.
4442
ast = CoffeeScript.nodes(tokens).body
4543
# Add assignment to `__` variable to force the input to be an expression.
@@ -49,7 +47,8 @@ replDefaults =
4947
isAsync = ast.isAsync
5048
# Invoke the wrapping closure.
5149
ast = new Root new Block [new Call ast]
52-
js = ast.compile {bare: yes, locals: Object.keys(context), referencedVars, sharedScope: yes}
50+
ast.findReferencedVars()
51+
js = ast.compile {bare: yes, locals: Object.keys(context), sharedScope: yes}
5352
if transpile
5453
js = transpile.transpile(js, transpile.options).code
5554
# Strip `"use strict"`, to avoid an exception on assigning to

src/scope.litcoffee

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ compiler-generated variable. `_var`, `_var2`, and so on...
9191
index = 0
9292
loop
9393
temp = @temporary name, index, options.single
94-
break unless @check(temp) or temp in @root.referencedVars
94+
break unless @check(temp) or @referencedVars.has(temp) or
95+
@shared and @parent.referencedVars.has(temp)
9596
index++
9697
@add temp, 'var', yes if options.reserve ? true
9798
temp

0 commit comments

Comments
 (0)