-
-
Notifications
You must be signed in to change notification settings - Fork 35.4k
repl: support mult-line string-keyed objects #21805
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
Changes from 1 commit
9affc6f
7bb9bf1
d426b00
974b337
02c5827
e58b861
d426d86
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
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.
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,10 +47,11 @@ const { | |
| makeRequireFunction, | ||
| addBuiltinLibsToObject | ||
| } = require('internal/modules/cjs/helpers'); | ||
| const acorn = require('internal/deps/acorn/dist/acorn'); | ||
| const { | ||
| isIdentifierStart, | ||
| isIdentifierChar | ||
| } = require('internal/deps/acorn/dist/acorn'); | ||
| } = acorn; | ||
| const internalUtil = require('internal/util'); | ||
| const { isTypedArray } = require('internal/util/types'); | ||
| const util = require('util'); | ||
|
|
@@ -1507,72 +1508,60 @@ function regexpEscape(s) { | |
|
|
||
| // If the error is that we've unexpectedly ended the input, | ||
| // then let the user try to recover by adding more input. | ||
| // Note: `e` (the original exception is not used by the current implemention, | ||
| // but may be needed in the future.) | ||
| function isRecoverableError(e, code) { | ||
| if (e && e.name === 'SyntaxError') { | ||
| var message = e.message; | ||
| if (message === 'Unterminated template literal' || | ||
| message === 'Unexpected end of input') { | ||
| return true; | ||
| } | ||
| let recoverable = false; | ||
|
|
||
| // Determine if the point of the any error raised is at the end of the input. | ||
| // There are two cases to consider: | ||
| // 1. Any error raised after we have encountered the 'eof' token. | ||
| // This prevents us from declaring partial tokens (like '2e') as | ||
| // recoverable. | ||
| // 2. Three cases where tokens can legally span lines. This is | ||
| // template, comment, and strings with a backslash at the end of | ||
| // the line, indicating a continuation. | ||
| acorn.plugins.repl = (parser) => { | ||
| parser.extend('nextToken', (nextToken) => { | ||
| return function(pos, message) { | ||
| nextToken.call(this, pos, message); | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For my education, can I ask why? First, I don't see
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| if (parser.type === acorn.tokTypes.eof) recoverable = true; | ||
| }; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe |
||
| }); | ||
|
|
||
| if (message === 'missing ) after argument list') { | ||
| const frames = e.stack.split(/\r?\n/); | ||
| const pos = frames.findIndex((f) => f.match(/^\s*\^+$/)); | ||
| return pos > 0 && frames[pos - 1].length === frames[pos].length; | ||
| } | ||
| parser.extend('raise', (raise) => { | ||
| return function(pos, message) { | ||
| switch (message) { | ||
| case 'Unterminated template': | ||
| case 'Unterminated comment': | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| recoverable = true; | ||
| break; | ||
|
|
||
| case 'Unterminated string constant': | ||
| const token = parser.input.slice(parser.lastTokStart, parser.pos); | ||
| recoverable = token.endsWith('\\\n'); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same nit as above, inconsistent use of |
||
| } | ||
|
|
||
| if (message === 'Invalid or unexpected token') | ||
| return isCodeRecoverable(code); | ||
| } | ||
| return false; | ||
| } | ||
| raise.call(this, pos, message); | ||
| }; | ||
| }); | ||
| }; | ||
|
|
||
| // Check whether a code snippet should be forced to fail in the REPL. | ||
| function isCodeRecoverable(code) { | ||
| var current, previous, stringLiteral; | ||
| var isBlockComment = false; | ||
| var isSingleComment = false; | ||
| var isRegExpLiteral = false; | ||
| var lastChar = code.charAt(code.length - 2); | ||
| var prevTokenChar = null; | ||
|
|
||
| for (var i = 0; i < code.length; i++) { | ||
| previous = current; | ||
| current = code[i]; | ||
|
|
||
| if (previous === '\\' && (stringLiteral || isRegExpLiteral)) { | ||
| current = null; | ||
| } else if (stringLiteral) { | ||
| if (stringLiteral === current) { | ||
| stringLiteral = null; | ||
| } | ||
| } else if (isRegExpLiteral && current === '/') { | ||
| isRegExpLiteral = false; | ||
| } else if (isBlockComment && previous === '*' && current === '/') { | ||
| isBlockComment = false; | ||
| } else if (isSingleComment && current === '\n') { | ||
| isSingleComment = false; | ||
| } else if (!isBlockComment && !isRegExpLiteral && !isSingleComment) { | ||
| if (current === '/' && previous === '/') { | ||
| isSingleComment = true; | ||
| } else if (previous === '/') { | ||
| if (current === '*') { | ||
| isBlockComment = true; | ||
| // Distinguish between a division operator and the start of a regex | ||
| // by examining the non-whitespace character that precedes the / | ||
| } else if ([null, '(', '[', '{', '}', ';'].includes(prevTokenChar)) { | ||
| isRegExpLiteral = true; | ||
| } | ||
| } else { | ||
| if (current.trim()) prevTokenChar = current; | ||
| if (current === '\'' || current === '"') { | ||
| stringLiteral = current; | ||
| } | ||
| } | ||
| } | ||
| // For similar reasons as `defaultEval`, wrap expressions starting with a | ||
| // curly brace with parenthesis. Note: only the open parenthesis is added | ||
| // here as the point is to test for potentially valid but incomplete | ||
| // expressions. | ||
| if (/^\s*\{/.test(code) && isRecoverableError(e, `(${code}`)) return true; | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| // Try to parse the code with acorn. If the parse fails, ignore the acorn | ||
| // error and return the recoverable status. | ||
| try { | ||
| acorn.parse(code, { plugins: { repl: true } }); | ||
| return true; | ||
| } catch (e) { | ||
| return recoverable; | ||
| } | ||
|
|
||
| return stringLiteral ? lastChar === '\\' : isBlockComment; | ||
| } | ||
|
|
||
| function Recoverable(err) { | ||
|
|
||
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.
Should these additions be placed near the other acorn work in
lib/internal/repl/?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.
I'm not sure what you mean by these additions here. The require isn't new, just the capturing of the name as
acornis new. More generally,isRecoverableErrorisn't new, just completely rewritten.Perhaps you are suggesting that
isRecoverableErrorbe split out into a separate file and placed intolib/internal/repl/? If so, I can do that.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.
I'm suggesting keeping the acorn extensions and other acorn bits closer together.