Skip to content
This repository was archived by the owner on Apr 14, 2022. It is now read-only.

Commit eb8b341

Browse files
authored
Improve line formatter behavior with unmatched close brackets, log warning (#393)
Fixes #388. Instead of throwing a generic exception, keep track of the unmatched token for later use, and just ignore it. When the line formatting is done, DocumentOnTypeFormatting can check to see if an unmatched token occurred before or in the line being formatted and report to the user a warning about the bad behavior. I also fixed the comment strings that claimed that the line formatter worked with one-indexed lines, which is no longer true after Alex's changes to this code.
1 parent bb02227 commit eb8b341

File tree

5 files changed

+70
-15
lines changed

5 files changed

+70
-15
lines changed

src/Analysis/Engine/Test/LineFormatterTests.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,14 @@ public void CommentAfterOperator() {
396396
AssertSingleLineFormat("a+# comment\nb", "a + # comment");
397397
}
398398

399+
[DataRow("def foo()):\n a+b", "a + b", 1, 4, ")", 0)]
400+
[DataRow("x = [1, 2]\nx += [3]]\na+b", "a + b", 2, 0, "]", 1)]
401+
[DataRow("x = { foo: bar } } }\na+b", "a + b", 1, 0, "}", 0)]
402+
[DataTestMethod, Priority(0)]
403+
public void UnmatchedBracket(string code, string expected, int line, int editStart, string unmatched, int unmatchedLine) {
404+
AssertSingleLineFormat(code, expected, line: line, editStart: editStart, unmatched: (unmatched, unmatchedLine));
405+
}
406+
399407
[DataRow("'a''b'", "'a' 'b'")]
400408
[DataRow("'a' 'b'", "'a' 'b'")]
401409
[DataRow("'''a''''''b'''", "'''a''' '''b'''")]
@@ -452,13 +460,17 @@ public void GrammarFile() {
452460
/// <param name="line">The line number to request to be formatted.</param>
453461
/// <param name="languageVersion">Python language version to format.</param>
454462
/// <param name="editStart">Where the edit should begin (i.e. when whitespace or a multi-line string begins a line).</param>
455-
public static void AssertSingleLineFormat(string text, string expected, int line = 0, PythonLanguageVersion languageVersion = PythonLanguageVersion.V37, int editStart = 0) {
463+
/// <param name="unmatched">A nullable tuple to check against the line formatter's UnmatchedToken.</param>
464+
public static void AssertSingleLineFormat(string text, string expected, int line = 0, PythonLanguageVersion languageVersion = PythonLanguageVersion.V37, int editStart = 0, (string, int)? unmatched = null) {
456465
Check.ArgumentNotNull(nameof(text), text);
457466
Check.ArgumentNotNull(nameof(expected), expected);
458467

459468
using (var reader = new StringReader(text)) {
460-
var edits = new LineFormatter(reader, languageVersion).FormatLine(line);
469+
var lineFormatter = new LineFormatter(reader, languageVersion);
470+
var edits = lineFormatter.FormatLine(line);
471+
461472
edits.Should().OnlyHaveTextEdit(expected, (line, editStart, line, text.Split('\n')[line].Length));
473+
lineFormatter.UnmatchedToken(line).Should().Be(unmatched);
462474
}
463475
}
464476

src/LanguageServer/Impl/Implementation/LineFormatter.cs

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
using System.IO;
2020
using System.Linq;
2121
using System.Text;
22-
using Microsoft.PythonTools;
2322
using Microsoft.PythonTools.Analysis;
2423
using Microsoft.PythonTools.Analysis.Infrastructure;
2524
using Microsoft.PythonTools.Parsing;
@@ -73,7 +72,7 @@ private void AddToken(TokenExt token) {
7372
/// next line to the first non-ignored token so that the formatter
7473
/// can look ahead.
7574
/// </summary>
76-
/// <param name="line">One-indexed line number.</param>
75+
/// <param name="line">Zero-indexed line number.</param>
7776
/// <param name="includeToken">A function which returns true if the token should be added to the final list. If null, all tokens will be added.</param>
7877
/// <returns>A non-null list of tokens on that line.</returns>
7978
private List<TokenExt> TokenizeLine(int line, Func<TokenExt, bool> includeToken = null) {
@@ -93,10 +92,25 @@ private List<TokenExt> TokenizeLine(int line, Func<TokenExt, bool> includeToken
9392
return _lineTokens.TryGetValue(line, out var tokens) ? tokens : new List<TokenExt>();
9493
}
9594

95+
/// <summary>
96+
/// Returns a human-readable message describing the first unmatched
97+
/// bracket found by the tokenizer at the time this function is called.
98+
/// </summary>
99+
/// <param name="line">Zero-indexed line number.</param>
100+
/// <returns>A string representation of the unmatched token and its zero-indexed line number, or null if none exists or hasn't been scanned yet.</returns>
101+
public (string token, int line)? UnmatchedToken(int line) {
102+
var unmatched = _tokenizer.Unmatched;
103+
if (unmatched == null || unmatched.Line > line) {
104+
return null;
105+
}
106+
107+
return (unmatched.ToString(), unmatched.Line);
108+
}
109+
96110
/// <summary>
97111
/// Formats a single line and returns TextEdits to replace the old text.
98112
/// </summary>
99-
/// <param name="line">One-indexed line number.</param>
113+
/// <param name="line">Zero-indexed line number.</param>
100114
/// <returns>A list of TextEdits needed to format the line.</returns>
101115
public TextEdit[] FormatLine(int line) {
102116
if (line < 0) {
@@ -122,7 +136,7 @@ public TextEdit[] FormatLine(int line) {
122136
startIdx = 1;
123137
builder.Append(' ');
124138
}
125-
139+
126140
for (var i = startIdx; i < tokens.Count; i++) {
127141
var token = tokens[i];
128142
var prev = tokens.ElementAtOrDefault(i - 1);
@@ -239,7 +253,7 @@ public TextEdit[] FormatLine(int line) {
239253
// Check unpacking case
240254
} else if (token.Kind == TokenKind.Multiply && (actualPrev == null || actualPrev.Kind != TokenKind.Name && actualPrev.Kind != TokenKind.Constant && !actualPrev.IsClose)) {
241255
builder.Append(token);
242-
} else {
256+
} else {
243257
AppendTokenEnsureWhiteSpacesAround(builder, token);
244258
}
245259
break;
@@ -360,16 +374,16 @@ public TextEdit[] FormatLine(int line) {
360374

361375
var edit = new TextEdit {
362376
range = new Range {
363-
start = new Position{ line = line, character = startIndex - lineStartIndex },
364-
end = new Position{ line = line, character = endIndex - lineStartIndex }
377+
start = new Position { line = line, character = startIndex - lineStartIndex },
378+
end = new Position { line = line, character = endIndex - lineStartIndex }
365379
},
366380
newText = newText
367381
};
368382

369383
return new[] { edit };
370384
}
371385

372-
private static void AppendTokenEnsureWhiteSpacesAround(StringBuilder builder, TokenExt token)
386+
private static void AppendTokenEnsureWhiteSpacesAround(StringBuilder builder, TokenExt token)
373387
=> builder.EnsureEndsWithWhiteSpace()
374388
.Append(token)
375389
.EnsureEndsWithWhiteSpace();
@@ -430,7 +444,7 @@ public bool MatchesClose(TokenExt other) {
430444
public bool IsKeyword => (Kind >= TokenKind.FirstKeyword && Kind <= TokenKind.LastKeyword) || Kind == TokenKind.KeywordAsync || Kind == TokenKind.KeywordAwait;
431445

432446
public bool IsString => Kind == TokenKind.Constant && Token != Tokens.NoneToken && (Token.Value is string || Token.Value is AsciiString);
433-
447+
434448
public bool IsSimpleSliceToLeft {
435449
get {
436450
if (Kind != TokenKind.Colon) {
@@ -535,6 +549,7 @@ private class TokenizerWrapper {
535549
private readonly Tokenizer _tokenizer;
536550
private readonly Stack<TokenExt> _insides = new Stack<TokenExt>();
537551
public TokenExt Current { get; private set; }
552+
public TokenExt Unmatched { get; private set; }
538553

539554
public TokenizerWrapper(Tokenizer tokenizer) {
540555
_tokenizer = tokenizer;
@@ -570,10 +585,17 @@ public TokenExt Next() {
570585
);
571586

572587
if (tokenExt.IsClose) {
573-
if (_insides.Count == 0 || !_insides.Peek().MatchesClose(tokenExt)) {
574-
throw new Exception($"Close bracket ({token.Kind}) has no matching open");
588+
if (_insides.Count > 0 && _insides.Peek().MatchesClose(tokenExt)) {
589+
_insides.Pop();
590+
} else {
591+
// This close doesn't match, so assume that the token is just a stray
592+
// and do nothing to the _insides stack.
593+
594+
// Keep track of the first unmatched token to present back to the user.
595+
if (Unmatched == null) {
596+
Unmatched = tokenExt;
597+
}
575598
}
576-
_insides.Pop();
577599
} else if (tokenExt.Kind == TokenKind.Colon && _insides.Count != 0 && _insides.Peek().Kind == TokenKind.KeywordLambda) {
578600
_insides.Pop();
579601
}

src/LanguageServer/Impl/Implementation/Server.OnTypeFormatting.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using System.Threading;
1919
using System.Threading.Tasks;
2020
using Microsoft.PythonTools.Analysis;
21+
using Microsoft.PythonTools.Analysis.Infrastructure;
2122

2223
namespace Microsoft.Python.LanguageServer.Implementation {
2324
public sealed partial class Server {
@@ -49,7 +50,15 @@ public override async Task<TextEdit[]> DocumentOnTypeFormatting(DocumentOnTypeFo
4950
}
5051

5152
var lineFormatter = new LineFormatter(reader, Analyzer.LanguageVersion);
52-
return lineFormatter.FormatLine(targetLine);
53+
var edits = lineFormatter.FormatLine(targetLine);
54+
var unmatchedToken = lineFormatter.UnmatchedToken(targetLine);
55+
56+
if (unmatchedToken != null) {
57+
var message = Resources.LineFormatter_UnmatchedToken.FormatInvariant(unmatchedToken.Value.token, unmatchedToken.Value.line + 1);
58+
LogMessage(MessageType.Warning, message);
59+
}
60+
61+
return edits;
5362
}
5463
}
5564
}

src/LanguageServer/Impl/Resources.Designer.cs

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

src/LanguageServer/Impl/Resources.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@
132132
<data name="LanguageServerVersion" xml:space="preserve">
133133
<value>Microsoft Python Language Server version {0}</value>
134134
</data>
135+
<data name="LineFormatter_UnmatchedToken" xml:space="preserve">
136+
<value>Unmatched token '{0}' on line {1}; line formatting may not be accurate.</value>
137+
</data>
135138
<data name="ReloadingModules" xml:space="preserve">
136139
<value>Reloading modules... </value>
137140
</data>

0 commit comments

Comments
 (0)