Skip to content

Commit 4a8f0e9

Browse files
committed
Don't clarify precedence when operators is the same
1 parent 762a8f0 commit 4a8f0e9

3 files changed

Lines changed: 105 additions & 64 deletions

File tree

src/parens.ts

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import * as vscode from "vscode";
2-
import { findParens } from "./parse";
2+
import { findParens, ParenLocations } from "./parse";
33
import { debounce, repeat } from "./utils";
44
import {
55
DEBOUNCE_CONFIG,
66
USE_FLOW_CONFIG,
77
PAREN_COLOR_ID,
88
SUPPORTED_LANGUAGE_IDS,
99
} from "./constants";
10-
import { ParserPlugin } from "@babel/parser";
1110

1211
export function activateParens() {
1312
const subscriptions = [];
@@ -27,12 +26,18 @@ export function activateParens() {
2726

2827
subscriptions.push(decorationType);
2928

29+
const outputChannel = vscode.window.createOutputChannel(
30+
"Implicit Parentheses"
31+
);
32+
subscriptions.push(outputChannel);
33+
3034
for (const editor of vscode.window.visibleTextEditors) {
31-
updateDecorations(editor, decorationType);
35+
updateDecorations(editor, decorationType, outputChannel);
3236
}
3337

3438
const triggerUpdateDecorations = debounce(
35-
(editor: vscode.TextEditor) => updateDecorations(editor, decorationType),
39+
(editor: vscode.TextEditor) =>
40+
updateDecorations(editor, decorationType, outputChannel),
3641
vscode.workspace.getConfiguration().get(DEBOUNCE_CONFIG)
3742
);
3843
subscriptions.push(
@@ -57,7 +62,14 @@ export function activateParens() {
5762
activeEditor !== undefined &&
5863
event.document === activeEditor.document
5964
) {
60-
triggerUpdateDecorations(activeEditor);
65+
const { languageId } = activeEditor.document;
66+
if (SUPPORTED_LANGUAGE_IDS.has(languageId)) {
67+
triggerUpdateDecorations(activeEditor);
68+
} else {
69+
outputChannel.appendLine(
70+
`The language ${languageId} is not supported by Implicit Parentheses`
71+
);
72+
}
6173
}
6274
},
6375
null,
@@ -69,24 +81,28 @@ export function activateParens() {
6981

7082
function updateDecorations(
7183
editor: vscode.TextEditor,
72-
decorationType: vscode.TextEditorDecorationType
84+
decorationType: vscode.TextEditorDecorationType,
85+
outputChannel: vscode.OutputChannel
7386
) {
74-
const { languageId } = editor.document;
75-
if (!SUPPORTED_LANGUAGE_IDS.has(languageId)) {
76-
// TODO: Log
77-
return;
78-
}
87+
outputChannel.appendLine("Updating decorations...");
88+
7989
const useFlow: boolean =
8090
vscode.workspace.getConfiguration().get(USE_FLOW_CONFIG) ?? false;
8191

82-
const parens = findParens(
83-
editor.document.getText(),
84-
editor.document.languageId,
85-
useFlow
86-
);
87-
if (parens === null) {
92+
let parens: ParenLocations;
93+
try {
94+
parens = findParens(
95+
editor.document.getText(),
96+
editor.document.languageId,
97+
useFlow
98+
);
99+
} catch (e) {
100+
outputChannel.appendLine(
101+
`Encounterd an while parsing ${editor.document.fileName}}: ${String(e)}`
102+
);
88103
return;
89104
}
105+
90106
const { open, close } = parens;
91107

92108
const decorations: {

src/parse.ts

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,22 @@ import traverse from "@babel/traverse";
33
import { File as FileAST, Node } from "@babel/types";
44
import { ParserOptions, ParserPlugin } from "@babel/parser";
55

6-
type ParenLocations = Map<number, number>;
7-
export type Parens = { open: ParenLocations; close: ParenLocations };
6+
type Locations = Map<number, number>;
7+
export type ParenLocations = { open: Locations; close: Locations };
88

99
const CONFUSING = new Set([
1010
"BinaryExpression",
1111
"LogicalExpression",
1212
"UnaryExpression",
1313
"ConditionalExpression",
14+
"AssignmentExpression",
1415
]);
1516

1617
export function findParens(
1718
text: string,
1819
languageId: string,
1920
useFlow: boolean
20-
): { open: ParenLocations; close: ParenLocations } | null {
21+
): ParenLocations {
2122
const openParens: number[] = [];
2223
const closeParens: number[] = [];
2324

@@ -29,40 +30,50 @@ export function findParens(
2930
closeParens.push(node.end);
3031
}
3132

32-
let ast;
33-
try {
34-
ast = parser.parse(text, babelOptions({ languageId, useFlow }));
35-
} catch (e) {
36-
return null;
37-
}
33+
const ast = parser.parse(text, babelOptions({ languageId, useFlow }));
3834

39-
const ASSOCIATIVE_BINARY_OPERATORS = new Set(["+", "*", "&", "|"]);
40-
const ASSOCIATIVE_LOGICAL_OPERATORS = new Set(["||", "&&", "??"]);
35+
const RIGHT_TO_LEFT_ASSOCIATIVE_OPERATORS = new Set([
36+
"**",
37+
"=",
38+
"+=",
39+
"-=",
40+
"%=",
41+
"**=",
42+
"*=",
43+
"<<=",
44+
">>=",
45+
">>>=",
46+
"&=",
47+
"^=",
48+
"|=",
49+
"&&=",
50+
"||=",
51+
"??=",
52+
]);
4153

4254
traverse(ast, {
4355
enter(path) {
4456
if (!CONFUSING.has(path.type) || !CONFUSING.has(path.parent.type)) {
4557
// People can figure these out probably
4658
return;
4759
}
48-
if (isUnaryNot(path.node) && isUnaryNot(path.parent)) {
49-
// !! is an idiom that people can figure out
50-
return;
51-
}
52-
if (
53-
path.node.type === path.parent.type &&
54-
// @ts-ignore
55-
path.node.operator === path.parent.operator
56-
) {
60+
if (path.node.type === path.parent.type) {
5761
if (
58-
(path.node.type === "BinaryExpression" &&
59-
ASSOCIATIVE_BINARY_OPERATORS.has(path.node.operator)) ||
60-
(path.node.type === "LogicalExpression" &&
61-
ASSOCIATIVE_LOGICAL_OPERATORS.has(path.node.operator))
62+
// @ts-ignore
63+
path.node.operator !== undefined &&
64+
// @ts-ignore
65+
path.node.operator === path.parent.operator &&
66+
// @ts-ignore
67+
!RIGHT_TO_LEFT_ASSOCIATIVE_OPERATORS.has(path.node.operator)
6268
) {
69+
// Here the presedence is obvisouly the same, and people can
70+
// infer left-to-right associativity.
6371
return;
6472
}
73+
} else if (path.parent.type === "AssignmentExpression") {
74+
return;
6575
}
76+
6677
addParens(path.node);
6778
},
6879
});
@@ -115,10 +126,6 @@ function groupLineNumbers(numbers: number[]) {
115126
return counts;
116127
}
117128

118-
function isUnaryNot(node: Node) {
119-
return node.type === "UnaryExpression" && node.operator === "!";
120-
}
121-
122129
// Stolen from Prettier: https://github.com/prettier/prettier/blob/797e93fc0a3a7f2ba2b510a1a246fc6bdbe89025/src/language-js/parser-babel.js#L18-L41
123130
function babelOptions({
124131
languageId,

src/test/unit/parser.test.ts

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as assert from "assert";
2-
import { findParens, Parens } from "../../parse";
2+
import { findParens } from "../../parse";
33
import { repeat } from "../../utils";
44

55
function makeSnapshot(
@@ -34,49 +34,67 @@ suite("Unit Test Suite", () => {
3434
test("Ignore parenthesized expressions", () => {
3535
assert.equal(makeSnapshot("1 + (1 * 1)"), "1 + (1 * 1)");
3636
});
37-
test("Ignore associative operators", () => {
37+
test("Ignore assignment mixed with other operators", () => {
38+
assert.equal(makeSnapshot("1 = 1 * 1"), "1 = 1 * 1");
39+
});
40+
41+
test("Cast to bool idiom", () => {
42+
assert.equal(makeSnapshot("!!foo"), "!!foo");
43+
assert.equal(makeSnapshot("+!!foo"), "+₍!!foo₎");
44+
});
45+
46+
test("Ignore identical operators if they are left-to-right associative", () => {
3847
assert.equal(makeSnapshot("1 + 1 + 1"), "1 + 1 + 1");
3948
assert.equal(makeSnapshot("1 * 1 * 1"), "1 * 1 * 1");
4049
assert.equal(makeSnapshot("1 && 1 && 1"), "1 && 1 && 1");
4150
assert.equal(makeSnapshot("1 || 1 || 1"), "1 || 1 || 1");
4251
assert.equal(makeSnapshot("1 ?? 1 ?? 1"), "1 ?? 1 ?? 1");
4352
assert.equal(makeSnapshot("1 & 1 & 1"), "1 & 1 & 1");
4453
assert.equal(makeSnapshot("1 | 1 | 1"), "1 | 1 | 1");
54+
assert.equal(makeSnapshot("1 - 1 - 1"), "1 - 1 - 1");
55+
assert.equal(makeSnapshot("1 - 1 - 1"), "1 - 1 - 1");
56+
});
57+
58+
test("Don't ignore identical operators if they are right-to-left associative", () => {
59+
assert.equal(makeSnapshot("1 ** 1 ** 1"), "1 ** ₍1 ** 1₎");
60+
assert.equal(makeSnapshot("a = b = c"), "a = ₍b = c₎");
61+
assert.equal(makeSnapshot("a ??= b ??= c"), "a ??= ₍b ??= c₎");
62+
});
63+
64+
test("Nested Ternary", () => {
65+
assert.equal(makeSnapshot("a ? b ? c : d : e"), "a ? ₍b ? c : d₎ : e");
66+
assert.equal(makeSnapshot("a ? b : c ? d : e"), "a ? b : ₍c ? d : e₎");
67+
// assert.equal(makeSnapshot("a ? b : c ? d : e"), "1 ** ₍1 ** 1₎");
4568
});
4669

4770
// Parser config tests
4871
test("Flow languageId", () => {
49-
assert.notEqual(
50-
makeSnapshot("type FooT = {| foo: string |};", "flow", false),
51-
null
72+
assert.doesNotThrow(() =>
73+
makeSnapshot("type FooT = {| foo: string |};", "flow", false)
5274
);
5375
});
5476
test("Flow as default for js", () => {
55-
assert.notEqual(
56-
makeSnapshot("type FooT = {| foo: string |};", "javascript", true),
57-
null
77+
assert.doesNotThrow(() =>
78+
makeSnapshot("type FooT = {| foo: string |};", "javascript", true)
5879
);
59-
assert.notEqual(
60-
makeSnapshot("type FooT = {| foo: string |};", "javascriptreact", true),
61-
null
80+
assert.doesNotThrow(() =>
81+
makeSnapshot("type FooT = {| foo: string |};", "javascriptreact", true)
6282
);
6383
});
6484
test("Typescript cannot parse Flow", () => {
65-
assert.equal(
66-
makeSnapshot("type FooT = {| foo: string |};", "typescript"),
67-
null
85+
assert.throws(() =>
86+
makeSnapshot("type FooT = {| foo: string |};", "typescript")
6887
);
6988
});
7089
test("TypeScript", () => {
71-
assert.notEqual(
72-
makeSnapshot("interface FooT { foo: string };", "typescript"),
73-
null
90+
assert.doesNotThrow(() =>
91+
makeSnapshot("interface FooT { foo: string };", "typescript")
7492
);
7593
});
7694

7795
test("JSX", () => {
78-
assert.equal(makeSnapshot("<Bar />", "typescript"), null);
79-
assert.equal(makeSnapshot("<Bar />", "javascript"), null);
96+
assert.throws(() => makeSnapshot("<Bar />", "typescript"));
97+
assert.throws(() => makeSnapshot("<Bar />", "javascript"));
8098
assert.equal(makeSnapshot("<Bar />", "typescriptreact"), "<Bar />");
8199
assert.equal(makeSnapshot("<Bar />", "javascriptreact"), "<Bar />");
82100
assert.equal(makeSnapshot("<Bar />", "flow"), "<Bar />");

0 commit comments

Comments
 (0)