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

Fix parser regression for bad related diagnostic on missing matching brackets #44158

Merged
merged 8 commits into from Mar 16, 2022

Conversation

jessetrinity
Copy link
Contributor

@jessetrinity jessetrinity commented May 19, 2021

The parser expects the keywords if, do, while, and with to be followed by pairs of braces({, }) or parentheses ((,)).

It was also attempting to unconditionally add a related diagnostic for missing closing brackets to the diagnostics for missing opening brackets. When the opening bracket did not exist, this typically meant that diagnostic span was being added beyond the EOF.

Here we avoid associating the missing closing bracket diagnostic with the opening bracket diagnostic if we fail to parse the former. The errors resulting from the regression broke smart indentation on the keywords, thus the indentation tests in this change. We don't have the same concern about parseExpectedMatchingBrackets with array and object literals since their parsings are triggered by their opening braces.

Regressed by #40884

@typescript-bot typescript-bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels May 19, 2021
@jessetrinity jessetrinity changed the title Fix parser regression for bad related diagnostic for matching brackets Fix parser regression for bad related diagnostic on missing matching brackets May 19, 2021
@jessetrinity
Copy link
Contributor Author

jessetrinity commented May 19, 2021

I'll admit up front that I don't fully understand the chain of events that led from the parser error to smart indentation breaking. While debugging I was seeing Debug.assert(!incrementalSourceFile.hasBeenIncrementallyParsed); in parser.ts throw when the getIndentation command ran, derailing it along with a handful of other language service command my editor was firing at the time. I assume that the bad diagnostic dirtied up something somewhere else in the parser and getting us into a bad state .-.

@DanielRosenwasser
Copy link
Member

While debugging I was seeing Debug.assert(!incrementalSourceFile.hasBeenIncrementallyParsed); in parser.ts throw when the getIndentation command ran, derailing it along with a handful of other language service command my editor was firing at the time. I assume that the bad diagnostic dirtied up something somewhere else in the parser and getting us into a bad state .-.

!!!

I would strongly urge a revert on the commit for 4.3.

const expression = allowInAnd(parseExpression);
parseExpectedMatchingBrackets(SyntaxKind.OpenParenToken, SyntaxKind.CloseParenToken, openParenPosition);
openParenExists ? parseExpectedMatchingBrackets(SyntaxKind.OpenParenToken, SyntaxKind.CloseParenToken, openParenPosition)
Copy link
Member

Choose a reason for hiding this comment

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

Can you make these if else statements instead.. this makes it very hard to read

const expression = allowInAnd(parseExpression);
parseExpectedMatchingBrackets(SyntaxKind.OpenParenToken, SyntaxKind.CloseParenToken, openParenPosition);
openParenExists ? parseExpectedMatchingBrackets(SyntaxKind.OpenParenToken, SyntaxKind.CloseParenToken, openParenPosition)
: parseExpected(SyntaxKind.CloseParenToken);
Copy link
Member

Choose a reason for hiding this comment

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

This is needed in parseObjectLiteralExpression?

I think instead of changing this in all use cases, why not modify parseExpectedMatchingBrackets to take in result of openingBracket so all cases are always covered and we dont need to review all use cases...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think it should be needed for object literal expression (or arrays) since we only try to parse them if an opening bracket has already been parsed (or scanned, not sure what the correct terminology is there).

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

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

I bet the easiest way to move forward with this PR is to create a new one that starts with a revert of your previous revert PR, then add back a commit with a fix.

src/compiler/parser.ts Show resolved Hide resolved
src/compiler/parser.ts Outdated Show resolved Hide resolved
Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

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

2 suggestions plus 1 wishful-thinking suggestion

src/compiler/parser.ts Show resolved Hide resolved
@@ -1552,6 +1555,23 @@ namespace ts {
return false;
}

function parseExpectedMatchingBrackets(openKind: SyntaxKind, closeKind: SyntaxKind, openParsed: boolean, openPosition: number) {
if (!openParsed) {
Copy link
Member

Choose a reason for hiding this comment

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

If openParsed is false, parseExpected is equivalent to the code before if (lastError).
if openParsed is true, the code before if (lastError) should run.

Equivalent code runs either way, so I'd move the openParsed check after const lastError = ...

If there was an elegant way to change parseExpected to return its error, I'd recommend replacing if (token() = ... through const lastError = ... with const lastError = parseExpected(.... But it's used all over the place with the current true | false return type.

Comment on lines 5573 to +5577
const openBracePosition = scanner.getTokenPos();
parseExpected(SyntaxKind.OpenBraceToken);
const openBraceParsed = parseExpected(SyntaxKind.OpenBraceToken);
const multiLine = scanner.hasPrecedingLineBreak();
const properties = parseDelimitedList(ParsingContext.ObjectLiteralMembers, parseObjectLiteralElement, /*considerSemicolonAsDelimiter*/ true);
if (!parseExpected(SyntaxKind.CloseBraceToken)) {
const lastError = lastOrUndefined(parseDiagnostics);
if (lastError && lastError.code === Diagnostics._0_expected.code) {
addRelatedInfo(
lastError,
createDetachedDiagnostic(fileName, openBracePosition, 1, Diagnostics.The_parser_expected_to_find_a_to_match_the_token_here)
);
}
}
parseExpectedMatchingBrackets(SyntaxKind.OpenBraceToken, SyntaxKind.CloseBraceToken, openBraceParsed, openBracePosition);
Copy link
Member

Choose a reason for hiding this comment

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

Having to repeat openBracePosition/openBraceParsed every time is still a bit noisy. You could eliminate it using a closure, but I don't think it's worth the performance cost. Still, uh, here's what it would look like:

const parseCloseBrace = parseExpectedMatchingToken(SyntaxKind.OpenBraceToken, SyntaxKind.CloseBraceToken)
const multiline = ...
const properties = ...
parseCloseBrace()

And then the first few lines of parseExpectedMatchingBrackets look like this:

        function parseExpectedMatchingBrackets(openKind: SyntaxKind, closeKind: SyntaxKind) {
            const openPosition = getNodePos();
            const openParsed = parseExpected(openKind);
            return () => {
                if (token() === closeKind) { // same as before, but inside a closure

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ahh, that's better. Why does it have significantly worse performance though?

Copy link
Member

Choose a reason for hiding this comment

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

You have to create a closure each time, which closes over openKind, closeKind, openPosition, openParsed. The naive way to compile this is to put them onto the heap, so the GC now has 4 numbers and a function to collect.

However, those are all numbers or booleans, and v8 might emit fast code. I have no idea; so it's probably worth testing if you like the change enough -- the existing perf suite certainly has enough brackets to stress it.

@sandersn sandersn added this to Not started in PR Backlog May 28, 2021
@sandersn sandersn moved this from Not started to Waiting on author in PR Backlog May 28, 2021
@@ -121,7 +121,7 @@ tests/cases/compiler/constructorWithIncompleteTypeAnnotation.ts(261,1): error TS
if (retValue != 0 ^= {
~~
!!! error TS1005: ')' expected.
!!! related TS1007 tests/cases/compiler/constructorWithIncompleteTypeAnnotation.ts:22:20: The parser expected to find a ')' to match the '(' token here.
!!! related TS1007 tests/cases/compiler/constructorWithIncompleteTypeAnnotation.ts:22:19: The parser expected to find a ')' to match the '(' token here.
Copy link
Member

Choose a reason for hiding this comment

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

Uh-oh, I didn't expect any baseline changes. I counted the spaces and 19 is correct counting from 0; 20 is correct counting from 1. I'm not sure which we do for chars, but I see lines counting from 1 in tests/baselines/reference/jsonParserRecovery/TypeScript_code.errors.txt

I think this change is wrong. Sorry for steering you wrong. =(

@sandersn sandersn assigned sandersn and unassigned jessetrinity Feb 24, 2022
@sandersn sandersn merged commit 111ca92 into microsoft:main Mar 16, 2022
PR Backlog automation moved this from Waiting on author to Done Mar 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
PR Backlog
  
Done
Development

Successfully merging this pull request may close these issues.

None yet

5 participants