Skip to content

Commit 6ed961c

Browse files
authored
Experimental ternaries (#953)
* introduction of experimentalTernaries * rearranging Conditionals to experiment with the formatting * cleanup and closer the format suggested by prettier * tuning up the breaking rules * breaking lines and indenting non experimental ternaries. * adding and removing parentheses in conditional if condition breaks * fixing build script * removing unneeded group * Fixing indentation within a FunctionCall * cleanup and documentation * Adding README information on Experimental Ternaries * Extra comment * more readable bracket selection * condition can be a Conditional if it's wrapped in a parentheses * not using hardline inside an ifBreak to avoid unexpected group break propagation * moving check for parent function outside the breaking group, fixing an edge case and the linter error * clean up code and adding group around falseExpression * removing unnecessary conditions and hardlines and cleaning up the documentation * Adding extra examples * undoing unnecessary change * group reorganisation that allows us to get rid of ifBreaks and hardlineWithoutBreakParent cleaning the resulting code * unneeded check for new line on falseExpression * cleaner output * cleaning up documentation * Adding spaces after `:` to align properly with the trueExpression * adding indentation if we fill a tab before falseExpression * undoing refactor for performance issues
1 parent e1ca377 commit 6ed961c

File tree

12 files changed

+2492
-39
lines changed

12 files changed

+2492
-39
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,19 @@ You might have a multi-version project, where different files are compiled with
206206
| ------- | --------------------- | ---------------------- |
207207
| None | `--compiler <string>` | `compiler: "<string>"` |
208208

209+
### Experimental Ternaries
210+
211+
Mimicking prettier's [new ternary formatting](https://prettier.io/blog/2023/11/13/curious-ternaries) for the community to try.
212+
213+
Valid options:
214+
215+
- `true` - Use curious ternaries, with the question mark after the condition.
216+
- `false` - Retain the default behavior of ternaries; keep question marks on the same line as the consequent.
217+
218+
| Default | CLI Override | API Override |
219+
| ------- | -------------------------- | ------------------------------- |
220+
| false | `--experimental-ternaries` | `experimentalTernaries: <bool>` |
221+
209222
## Integrations
210223

211224
### Vim

src/binary-operator-printers/logical.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@ const { group, line, indent } = doc.builders;
55
const groupIfNecessaryBuilder = (path) => (document) =>
66
path.getParentNode().type === 'BinaryOperation' ? document : group(document);
77

8-
const indentIfNecessaryBuilder = (path) => (document) => {
8+
const indentIfNecessaryBuilder = (path, options) => (document) => {
99
let node = path.getNode();
1010
for (let i = 0; ; i += 1) {
1111
const parentNode = path.getParentNode(i);
1212
if (parentNode.type === 'ReturnStatement') return document;
1313
if (parentNode.type === 'IfStatement') return document;
1414
if (parentNode.type === 'WhileStatement') return document;
15+
if (
16+
options.experimentalTernaries &&
17+
parentNode.type === 'Conditional' &&
18+
parentNode.condition === node
19+
)
20+
return document;
1521
if (parentNode.type !== 'BinaryOperation') return indent(document);
1622
if (node === parentNode.right) return document;
1723
node = parentNode;
@@ -20,9 +26,9 @@ const indentIfNecessaryBuilder = (path) => (document) => {
2026

2127
export const logical = {
2228
match: (op) => ['&&', '||'].includes(op),
23-
print: (node, path, print) => {
29+
print: (node, path, print, options) => {
2430
const groupIfNecessary = groupIfNecessaryBuilder(path);
25-
const indentIfNecessary = indentIfNecessaryBuilder(path);
31+
const indentIfNecessary = indentIfNecessaryBuilder(path, options);
2632

2733
const right = [node.operator, line, path.call(print, 'right')];
2834
// If it's a single binary operation, avoid having a small right

src/nodes/Conditional.js

Lines changed: 83 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,88 @@
11
import { doc } from 'prettier';
2+
import { printSeparatedItem } from '../common/printer-helpers.js';
23

3-
const { group, indent, line } = doc.builders;
4+
const { group, hardline, ifBreak, indent, line, softline } = doc.builders;
45

5-
export const Conditional = {
6-
print: ({ path, print }) =>
7-
group([
8-
path.call(print, 'condition'),
9-
indent([
10-
line,
11-
'? ',
12-
path.call(print, 'trueExpression'),
13-
line,
14-
': ',
15-
path.call(print, 'falseExpression')
16-
])
6+
let groupIndex = 0;
7+
const experimentalTernaries = (node, path, print, options) => {
8+
const parent = path.getParentNode();
9+
const isNested = parent.type === 'Conditional';
10+
const isNestedAsTrueExpression = isNested && parent.trueExpression === node;
11+
const falseExpressionIsNested = node.falseExpression.type === 'Conditional';
12+
13+
// If the `condition` breaks into multiple lines, we add parentheses,
14+
// unless it already is a `TupleExpression`.
15+
const condition = path.call(print, 'condition');
16+
const conditionDoc = group([
17+
node.condition.type === 'TupleExpression'
18+
? condition
19+
: ifBreak(['(', printSeparatedItem(condition), ')'], condition),
20+
' ?'
21+
]);
22+
23+
// To switch between "case-style" and "curious" ternaries we force a new line
24+
// before a nested `trueExpression` if the current `Conditional` is also a
25+
// `trueExpression`.
26+
const trueExpressionDoc = indent([
27+
isNestedAsTrueExpression ? hardline : line,
28+
path.call(print, 'trueExpression')
29+
]);
30+
31+
const conditionAndTrueExpressionGroup = group(
32+
[conditionDoc, trueExpressionDoc],
33+
{ id: `Conditional.trueExpressionDoc-${groupIndex}` }
34+
);
35+
36+
groupIndex += 1;
37+
38+
// For the odd case of `tabWidth` of 1 or 0 we initiate `fillTab` as a single
39+
// space.
40+
let fillTab = ' ';
41+
if (
42+
!falseExpressionIsNested && // avoid processing if it's not needed
43+
(options.tabWidth > 2 || options.useTabs)
44+
) {
45+
fillTab = options.useTabs ? '\t' : ' '.repeat(options.tabWidth - 1);
46+
}
47+
48+
// A nested `falseExpression` is always printed in a new line.
49+
const falseExpression = path.call(print, 'falseExpression');
50+
const falseExpressionDoc = [
51+
isNested ? hardline : line,
52+
':',
53+
falseExpressionIsNested
54+
? [' ', falseExpression]
55+
: ifBreak([fillTab, indent(falseExpression)], [' ', falseExpression], {
56+
// We only add `fillTab` if we are sure the trueExpression is indented
57+
groupId: conditionAndTrueExpressionGroup.id
58+
})
59+
];
60+
61+
const document = group([conditionAndTrueExpressionGroup, falseExpressionDoc]);
62+
63+
return parent.type === 'VariableDeclarationStatement'
64+
? indent([softline, document])
65+
: document;
66+
};
67+
68+
const traditionalTernaries = (path, print) =>
69+
group([
70+
path.call(print, 'condition'),
71+
indent([
72+
// Nested trueExpression and falseExpression are always printed in a new
73+
// line
74+
path.getParentNode().type === 'Conditional' ? hardline : line,
75+
'? ',
76+
path.call(print, 'trueExpression'),
77+
line,
78+
': ',
79+
path.call(print, 'falseExpression')
1780
])
81+
]);
82+
83+
export const Conditional = {
84+
print: ({ node, path, print, options }) =>
85+
options.experimentalTernaries
86+
? experimentalTernaries(node, path, print, options)
87+
: traditionalTernaries(path, print)
1888
};

src/nodes/ReturnStatement.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,20 @@ import { doc } from 'prettier';
22

33
const { group, indent, line } = doc.builders;
44

5-
const expression = (node, path, print) => {
5+
const expression = (node, path, print, options) => {
66
if (node.expression) {
7-
return node.expression.type === 'TupleExpression'
7+
return node.expression.type === 'TupleExpression' ||
8+
(options.experimentalTernaries && node.expression.type === 'Conditional')
89
? [' ', path.call(print, 'expression')]
910
: group(indent([line, path.call(print, 'expression')]));
1011
}
1112
return '';
1213
};
1314

1415
export const ReturnStatement = {
15-
print: ({ node, path, print }) => [
16+
print: ({ node, path, print, options }) => [
1617
'return',
17-
expression(node, path, print),
18+
expression(node, path, print, options),
1819
';'
1920
]
2021
};

src/nodes/TupleExpression.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ const { group } = doc.builders;
66
const contents = (node, path, print) =>
77
node.components?.length === 1 && node.components[0].type === 'BinaryOperation'
88
? path.map(print, 'components')
9-
: [printSeparatedList(path.map(print, 'components'))];
9+
: printSeparatedList(path.map(print, 'components'));
1010

1111
export const TupleExpression = {
1212
print: ({ node, path, print }) =>
1313
group([
1414
node.isArray ? '[' : '(',
15-
...contents(node, path, print),
15+
contents(node, path, print),
1616
node.isArray ? ']' : ')'
1717
])
1818
};

src/options.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const CATEGORY_GLOBAL = 'Global';
22
const CATEGORY_COMMON = 'Common';
3+
const CATEGORY_JAVASCRIPT = 'JavaScript';
34
const CATEGORY_SOLIDITY = 'Solidity';
45

56
const options = {
@@ -40,6 +41,15 @@ const options = {
4041
default: false,
4142
description: 'Use single quotes instead of double quotes.'
4243
},
44+
experimentalTernaries: {
45+
category: CATEGORY_JAVASCRIPT,
46+
type: 'boolean',
47+
default: false,
48+
description:
49+
'Use curious ternaries, with the question mark after the condition.',
50+
oppositeDescription:
51+
'Default behavior of ternaries; keep question marks on the same line as the consequent.'
52+
},
4353
compiler: {
4454
category: CATEGORY_SOLIDITY,
4555
type: 'string',

src/parser.js

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,32 @@ const tryHug = (node, operators) => {
1414
return node;
1515
};
1616

17+
// The parser wrongly groups nested Conditionals in the falseExpression
18+
// in the following way:
19+
//
20+
// (a ? b : c) ? d : e;
21+
//
22+
// By reorganizing the group we have more flexibility when printing:
23+
//
24+
// a ? b : (c ? d : e);
25+
//
26+
// this is closer to the executed code and prints the same output.
27+
const rearrangeConditional = (ctx) => {
28+
while (ctx.condition.type === 'Conditional') {
29+
const falseExpression = {
30+
type: 'Conditional',
31+
condition: ctx.condition.falseExpression,
32+
trueExpression: ctx.trueExpression,
33+
falseExpression: ctx.falseExpression
34+
};
35+
rearrangeConditional(falseExpression);
36+
37+
ctx.falseExpression = falseExpression;
38+
ctx.trueExpression = ctx.condition.trueExpression;
39+
ctx.condition = ctx.condition.condition;
40+
}
41+
};
42+
1743
function parse(text, _parsers, options = _parsers) {
1844
const compiler = coerce(options.compiler);
1945
const parsed = parser.parse(text, { loc: true, range: true });
@@ -57,9 +83,21 @@ function parse(text, _parsers, options = _parsers) {
5783
ctx.loopExpression.omitSemicolon = true;
5884
},
5985
HexLiteral(ctx) {
60-
ctx.value = options.singleQuote
61-
? `hex'${ctx.value.slice(4, -1)}'`
62-
: `hex"${ctx.value.slice(4, -1)}"`;
86+
const value = ctx.value.slice(4, -1);
87+
ctx.value = options.singleQuote ? `hex'${value}'` : `hex"${value}"`;
88+
},
89+
Conditional(ctx) {
90+
rearrangeConditional(ctx);
91+
// We can remove parentheses only because we are sure that the
92+
// `condition` must be a single `bool` value.
93+
while (
94+
ctx.condition.type === 'TupleExpression' &&
95+
!ctx.condition.isArray &&
96+
ctx.condition.components.length === 1 &&
97+
ctx.condition.components[0].type !== 'Conditional'
98+
) {
99+
[ctx.condition] = ctx.condition.components;
100+
}
63101
},
64102
BinaryOperation(ctx) {
65103
switch (ctx.operator) {

test.config.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,10 @@ export default {
1616
filename: 'test.cjs',
1717
path: path.resolve(__dirname, 'dist'),
1818
globalObject: `
19-
typeof globalThis !== "undefined"
20-
? globalThis
21-
: typeof global !== "undefined"
22-
? global
23-
: typeof self !== "undefined"
24-
? self
25-
: this || {}
19+
typeof globalThis !== "undefined" ? globalThis
20+
: typeof global !== "undefined" ? global
21+
: typeof self !== "undefined" ? self
22+
: this || {}
2623
`,
2724
library: {
2825
export: 'default',

0 commit comments

Comments
 (0)