Skip to content
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

repl: support mult-line string-keyed objects #21805

Closed
wants to merge 7 commits into from

Conversation

rubys
Copy link
Member

@rubys rubys commented Jul 13, 2018

isRecoverableError is completely reimplemented using acorn and an
acorn plugin that examines the state of the parser at the time of the
error to determine if the code could be completed on a subsequent line.

fixes #21657

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • commit message follows commit guidelines

@nodejs-github-bot nodejs-github-bot added the repl Issues and PRs related to the REPL subsystem. label Jul 13, 2018
@vsemozhetbyt
Copy link
Contributor

lib/repl.js Outdated
@@ -47,10 +47,11 @@ const {
makeRequireFunction,
addBuiltinLibsToObject
} = require('internal/modules/cjs/helpers');
const acorn = require('internal/deps/acorn/dist/acorn');
const {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these additions be placed near the other acorn work in lib/internal/repl/?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what you mean by these additions here. The require isn't new, just the capturing of the name as acorn is new. More generally, isRecoverableError isn't new, just completely rewritten.

Perhaps you are suggesting that isRecoverableError be split out into a separate file and placed into lib/internal/repl/? If so, I can do that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm suggesting keeping the acorn extensions and other acorn bits closer together.

lib/repl.js Outdated
nextToken.call(this, pos, message);

if (parser.type === acorn.tokTypes.eof) recoverable = true;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

acorn.tokTypes is commonly stored in a var called tt in acorn plugins.
So then it would be tt.eof.

lib/repl.js Outdated
nextToken.call(this, pos, message);

if (parser.type === acorn.tokTypes.eof) recoverable = true;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe parse.type can be this.type

lib/repl.js Outdated
return function(pos, message) {
switch (message) {
case 'Unterminated template':
case 'Unterminated comment':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👆 Might put a note that these message strings are subject to change and to periodically check in on them.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll ponder how to word that. Meanwhile, I'll note that should these message strings change, a test will fail.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to manually update acorn to run into this issue. Having a failed test in such a circumstance would be fine and we'd have to invest that one way or the other. So I guess it is fine to keep it is as.

lib/repl.js Outdated
// here as the point is to test for potentially valid but incomplete
// expressions.
if (/^\s*\{/.test(code) && isRecoverableError(e, `(${code}`)) return true;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👆 Acorn has a thorough regexp for whitespace. Might use that for the leading optional whitespace check.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I think it is very important that the regular expression exactly match the one in defaultEval as it would be problematic for this code to treat a given input as recoverable only to later find out that defaultEval would consider it to be a syntax error when done.

lib/repl.js Outdated

case 'Unterminated string constant':
const token = parser.input.slice(parser.lastTokStart, parser.pos);
recoverable = token.endsWith('\\\n');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same nit as above, inconsistent use of parser vs. this.

lib/repl.js Outdated
parser.extend('nextToken', (nextToken) => {
return function(pos, message) {
nextToken.call(this, pos, message);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of nextToken.call and other .call's we should have const ReflectApply = Reflect.apply and then use it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my education, can I ask why? First, I don't see const ReflectApply = Reflect.apply as a common pattern inside of the node codebase (I only found two occurrences, and even those two differed). Second, I believe that .call is more concise and clearer (at a minimum, I wouldn't need to put [] around the arguments). What am I missing?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since core is not run in its own isolated context but in the one shared with the user, its builtins are the same ones that user-land can touch and interact with. Grabbing references to builtin prototype methods and static builtin methods before user-land code executes is an attempt to strengthen the core against accidental user-land augmentation. While handled inconsistently in Node core, new code added seems to have these guards. It's reasonable, where possible, to keep that up.

The use of ReflectApply covers the Function#call case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdalton there was a long discussion around this but no conclusion. As long as this is not something everyone agrees to I think it is best to keep these things to the PR openers.

Copy link
Member

@BridgeAR BridgeAR left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Great work @rubys and thanks a lot for taking up my suggestion.

lib/repl.js Outdated
parser.extend('nextToken', (nextToken) => {
return function(pos, message) {
nextToken.call(this, pos, message);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdalton there was a long discussion around this but no conclusion. As long as this is not something everyone agrees to I think it is best to keep these things to the PR openers.

lib/repl.js Outdated
return function(pos, message) {
switch (message) {
case 'Unterminated template':
case 'Unterminated comment':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to manually update acorn to run into this issue. Having a failed test in such a circumstance would be fine and we'd have to invest that one way or the other. So I guess it is fine to keep it is as.

@BridgeAR
Copy link
Member

Copy link
Member

@TimothyGu TimothyGu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome. Thanks!

// change these messages in the future, this will lead to a test
// failure, indicating that this code needs to be updated.
//
acorn.plugins.repl = (parser) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: replRecoverable maybe?

@addaleax
Copy link
Member

@addaleax addaleax added the author ready PRs that have at least one approval, no pending requests for changes, and a CI started. label Jul 18, 2018

case 'Unterminated string constant':
const token = this.input.slice(this.lastTokStart, this.pos);
recoverable = token.endsWith('\\\n');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👆 Line continuations allow more than just \ followed by \n. See this bit of this.
This might be hit by folks copy-pasting code chunks in.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I think it could be more than just Windows users (and potentially more than acorn currently supports). HTML specifies \r\n terminators for form input. And the ECMA standard defines more than \r and \n.

Copy link
Member

@jdalton jdalton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change requested here for Windows user considerations.

const token = this.input.slice(this.lastTokStart, this.pos);
// see https://www.ecma-international.org/ecma-262/#sec-line-terminators
recoverable = token.match(/\\(\r\n?|\n|\u2028|\u2029)$/);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

token.match() will return an array or null.
You probably want /\\(?:\r\n?|\n|\u2028|\u2029)$/.test(token).

Copy link
Contributor

@sagirk sagirk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🎉, with a minor nit.

// but Acorn detected no issue. Presume that additional text won't
// address this issue.
return false;
} catch (e) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: since e is not being used inside the catch block, can we get rid of it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have to use them until eslint updates to support optional catch bindings and we update to that eslint.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems to work just fine:

$ git diff
diff --git a/lib/internal/repl/recoverable.js b/lib/internal/repl/recoverable.js
index 29411d6534..465d77451a 100644
--- a/lib/internal/repl/recoverable.js
+++ b/lib/internal/repl/recoverable.js
@@ -69,7 +69,7 @@ function isRecoverableError(e, code) {
     // but Acorn detected no issue.  Presume that additional text won't
     // address this issue.
     return false;
-  } catch (e) {
+  } catch {
     return recoverable;
   }
 }
$ make jslint
Running JS linter...
Please use lint-js instead of jslint

Should I push it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it's fine. We have babel-eslint and already use optional catch bindings in the codebase

@trivikr
Copy link
Member

trivikr commented Aug 1, 2018

CI: https://ci.nodejs.org/job/node-test-pull-request/16117/

@Trott
Copy link
Member

Trott commented Aug 1, 2018

@trivikr
Copy link
Member

trivikr commented Aug 2, 2018

@rubys This PR needs a rebase from upstream/master.
The instructions are described in Step 5

isRecoverableError is completely reimplemented using acorn and an
acorn plugin that examines the state of the parser at the time of the
error to determine if the code could be completed on a subsequent line.
Added comments for regular expressions that need to match and
comparisons against exception message strings produced by Acorn.
@rubys
Copy link
Member Author

rubys commented Aug 2, 2018

There wasn't a conflict with master, but I rebased, tested, and pushed it anyway.

@Trott
Copy link
Member

Trott commented Aug 2, 2018

@trivikr
Copy link
Member

trivikr commented Aug 3, 2018

@Trott
Copy link
Member

Trott commented Aug 4, 2018

Landed in a2ec808

@Trott Trott closed this Aug 4, 2018
Trott pushed a commit to Trott/io.js that referenced this pull request Aug 4, 2018
isRecoverableError is completely reimplemented using acorn and an
acorn plugin that examines the state of the parser at the time of the
error to determine if the code could be completed on a subsequent line.

PR-URL: nodejs#21805
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
Reviewed-By: John-David Dalton <john.david.dalton@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
targos pushed a commit that referenced this pull request Aug 4, 2018
isRecoverableError is completely reimplemented using acorn and an
acorn plugin that examines the state of the parser at the time of the
error to determine if the code could be completed on a subsequent line.

PR-URL: #21805
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
Reviewed-By: John-David Dalton <john.david.dalton@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
author ready PRs that have at least one approval, no pending requests for changes, and a CI started. repl Issues and PRs related to the REPL subsystem.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Node REPL says "SyntaxError: unexpected symbol :" when parsing multi-line string-keyed objects (e.g. JSON)