-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix #4544: postfix conditional on first line of implicit object #4554
Fix #4544: postfix conditional on first line of implicit object #4554
Conversation
Does this fix any of the other lexer or related open issues? Thanks for tackling this! |
Wow! Just glancing over the diff I have no idea what's going on, but it all looks sensible. Good job! (I guess 😉 ) |
Yes, @lydell echoes my thoughts exactly ;) Just merge this? |
@GeoffreyBooth yup figured this was a good excuse to jump into the belly of the beast and understand how @lydell @GeoffreyBooth I'll go through now and leave specific comments in case you do want to vet anything more closely but I appreciate your trust that I'm not doing anything nonsensical |
@helixbass thanks! Yes, that was the idea behind the tagging. You might also want to skim through the “Invalid JS”-tagged ones, they might be related to the lexer/rewriter stuff. I don’t know how to get better notifications. I watch both repos but I only get emails when I’m mentioned or I’ve already participated in a thread. If you find a better way please let me know. On a related note, the plan is to merge I’ve never worked in the rewriter, so you know it far better than I do. Reading your code there’s nothing sloppy and nothing obviously wrong, so if you say it fixes the bug and doesn’t break any tests, I’m kind of taking your word for it 😄 |
@@ -559,7 +560,9 @@ exports.Lexer = class Lexer | |||
else if tok[0] is '(' | |||
tok[0] = 'PARAM_START' | |||
return this | |||
else return this | |||
else | |||
paramEndToken[0] = 'CALL_END' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
clean up after ourselves here rather than force the rewriter to (non-obviously) adjust a mismatched CALL_START
/PARAM_END
pair into CALL_START
/CALL_END
(in closeOpenCalls()
). This is for cases like f() ->
where here in tagParameters()
we start trying to back-label the parens as PARAM_START
/PARAM_END
but realize we're not actually looking at params but rather an immediately preceding call
if token[0] in EXPRESSION_START | ||
levels += 1 | ||
else if token[0] in EXPRESSION_END | ||
levels -= 1 | ||
if levels < 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is combining a cleanup of the existing logic with the addition of a new option
Cleanup: you can see that the if not token
in the line removed above could never be true, so you can move that line down here (simplifying the condition to just if levels < 0
) and not have to do the awkward i - 1
call at the beginning of the next iteration
New option: then just additionally check for the new returnOnNegativeLevel
option param, which allows you to override detectEnd()
's default behavior which is to also perform the specified action
if levels < 0
(ie we hit an EXPRESSION_END
we didn't see the start of)
closeOpenCalls: -> | ||
condition = (token, i) -> | ||
token[0] in [')', 'CALL_END'] or | ||
token[0] is 'OUTDENT' and @tag(i - 1) is ')' | ||
token[0] in [')', 'CALL_END'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this "mis-nested outdent" case seems to predate the behavior of eg pair()
in the lexer, which prevents mis-nested outdents from making it this far, so we can safely get rid of it. Also, as mentioned in the above lexer.coffee
comment, this method (closeOpenCalls()
) also now doesn't have to clean up mismatched CALL_START
/PARAM_END
pairs, which is nice b/c the way it was cleaning them up was totally non-obvious (relied on the detectEnd()
behavior described above of calling the action
when levels < 0
, in this case when it hit the incorrectly labeled PARAM_END
)
@@ -159,31 +159,36 @@ exports.Rewriter = class Rewriter | |||
# class declaration or if-conditionals) | |||
inImplicitControl = -> inImplicit() and stackTop()?[0] is 'CONTROL' | |||
|
|||
startImplicitCall = (j) -> | |||
idx = j ? i | |||
startImplicitCall = (idx) -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can safely remove all this j?
-related logic here and in startImplicitObject()
b/c all calls of these methods will clearly pass a numeric first argument
@@ -197,7 +202,7 @@ exports.Rewriter = class Rewriter | |||
# 1. We have seen a `CONTROL` argument on the line. | |||
# 2. The last token before the indent is part of the list below | |||
# | |||
if prevTag not in ['=>', '->', '[', '(', ',', '{', 'TRY', 'ELSE', '='] | |||
if prevTag not in ['=>', '->', '[', '(', ',', '{', 'ELSE', '='] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
safe to remove TRY
from this list b/c an implicit call could never be at the top of the stack (ie inImplicitCall()
could never be true) when we've just seen TRY
b/c the preceding clause would've caused a CONTROL
to get pushed onto the stack (in the previous scanTokens()
iteration). Effectively, this list can safely be mutually exclusive from the list above (['IF', 'TRY', ...]
)
@@ -236,13 +241,6 @@ exports.Rewriter = class Rewriter | |||
# a: b | |||
# c: d | |||
# | |||
# and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this comment seems to reference an old syntax that no longer compiles (now the first arg has to be an implicit object in order for indentation to imply an implicit call?)
@@ -316,7 +314,8 @@ exports.Rewriter = class Rewriter | |||
# Close implicit objects such as: | |||
# return a: 1, b: 2 unless true | |||
else if inImplicitObject() and not @insideForDeclaration and sameLine and | |||
tag isnt 'TERMINATOR' and prevTag isnt ':' | |||
tag isnt 'TERMINATOR' and prevTag isnt ':' and | |||
not (tag is 'POST_IF' and startsLine and implicitObjectContinues(i + 1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the actual fix. If we see a POST_IF
on the first line of an implicit object, check if it looks like that implicit object is actually multi-line rather than assuming it's an inline-implicit-object-ending POST_IF
|
||
endImplicitObject = (j) -> | ||
j = j ? i | ||
stack.pop() | ||
tokens.splice j, 0, generate '}', '}', token | ||
i += 1 | ||
|
||
implicitObjectContinues = (j) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks ahead (from the POST_IF
in question) for a following line that should be treated as part of the same implicit object (ie is at the same indentation level, is the next line (after any properly balanced intervening nested stuff), and is itself looking like an object literal)
@@ -439,6 +439,12 @@ test "#3216: For loop declaration as a value of an implicit object", -> | |||
arrayEq ob.b, test | |||
arrayEq ob.c, test | |||
arrayEq ob.d, test | |||
byFirstKey = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added a couple tests since the @insideForDeclaration
/#3216 stuff really only kicks in on the first line of an implicit object
Well, I’m convinced! I’d say let’s just merge it! |
|
||
endImplicitObject = (j) -> | ||
j = j ? i | ||
stack.pop() | ||
tokens.splice j, 0, generate '}', '}', token | ||
i += 1 | ||
|
||
implicitObjectContinues = (j) => | ||
nextTerminatorIdx = null | ||
@detectEnd j, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How costly is that lookahead here? It's called every single time we rewrite an implicit object literal, right? Is there a perf. impact?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@vendethiel nope only gets called when it sees a POST_IF
on the first line of an implicit object literal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the explanation.
Fixes #4544. Also includes some little
rewriter.coffee
cleanupsThe reason for the behavior @couchand describes in #4544 is that postfix
if
s are currently treated as implicit-object-literal-closing if they're on the first line of the object literal so that things likex = a: b if c
work correctly. But it's assuming that it's a single-line object literal, so if the expected compilation described by @couchand is desired (which I think it is, ie you should be able to swap two lines of an object literal without anything weird happening) then this fixes it by sniffing ahead to see if we appear to be on the first line of a multi-line object literalAdded the examples given by @couchand as well as a few more cases to
test/objects.coffee
@GeoffreyBooth I figured you'd want this targeted against
2
?@GeoffreyBooth @lydell let me know if I should justify the correctness of any/all of the little cleanup refactorings