RFC: Semicolon constraints and opinions
gitKrystan opened this issue · comments
(Thanks to @wycats, @chancancode, and @dfreeman for working through the logic behind this with me.)
Since Prettier is an opinionated code formatter, I'd like to settle on some ~*opinions*~
regarding when a semicolon should follow the closing </template>
tag.
In (almost) all of the examples in the First-Class Component Templates RFC and the ember-template-imports README, semicolons are not included after the closing </template>
tag. Indeed it is reasonable to conclude that omitting the semicolons is prettier, but there are some risks to this strategy as outlined below.
tl;dr
I recommend omitting and including semicolons as follows in this plugin in order to most closely match Prettier's handling of similar types of expressions:
export default class MyComponent extends Component {
<template>Hello</template> // omit
}
<template>Hello</template> // omit
export default <template>Hello</template> // omit
export const MyComponent = <template>Hello</template>; // include by default, omit in no-semi mode
With that said, there are some edge cases regarding "ambiguous expressions" following the template tag that may need to be handled with a combination of syntax errors, semicolons, or "cuddling."
Analysis
Given that we want the behavior of this Prettier plugin to closely match Prettier’s existing behavior, it’s useful to find an analogous syntax, then analyze Prettier’s behavior when formatting that syntax. Thus, in the examples below we will look at Prettier’s behavior when formatting functions and methods, which are arguably the closest plain-JavaScript equivalents to the template tag and can be used in the same positions.
According to the RFC, the template tag can occur in three distinct, legal positions. For each position, I’ve analyzed Prettier’s existing behavior for the analogs in the position and made recommendations regarding adding or omitting semicolons based on this analysis.
(For the purposes of this analysis, I'm defining "Ambiguous Expression" to mean that the next token could be the start of a new expression OR the continuation of the current expression. Learn more here.)
This analysis aims to answer the following questions for each position:
- Should a semicolon be included if the following token is the end of the file or otherwise unambiguous?
- Should a semicolon be included if the following token is ambiguous AND the developer did NOT include a semicolon in the original text?
- Should a semicolon be included if the following token is ambiguous AND the developer DID include a semicolon in the original text?
Top-Level Module
The template tag can be used as a top-level module declaration as shown below. I recommend omitting the semicolon in this case based on my analysis.
// template-only-component.gjs
<template>Hello</template>
// or
export default <template>Hello</template>
For analysis, we’ll look at how Prettier formats the analogous FunctionDeclaration
production, which is generally not followed by a semicolon.
Analysis Details
Case: Unambiguous, semi: true
Recommendation: Omit semicolon
// INPUT
<template>Hello</template>
// or
export default <template>Hello</template>
// OUTPUT
<template>Hello</template>
// or
export default <template>Hello</template>
Case: Unambiguous, semi: false
Recommendation: Omit semicolon
// INPUT
<template>Hello</template>
// or
export default <template>Hello</template>
// OUTPUT
<template>Hello</template>
// or
export default <template>Hello</template>
Case: Ambiguous, Dev omits semi, semi: true
Recommendation: Omit semicolon (with caveats as described below)
// INPUT
<template>Hello</template>
['oops']
// or
export default <template>Hello</template>
['oops']
// IDEAL OUTPUT (see ANALYSIS)
<template>Hello</template>
['oops'];
// or
export default <template>Hello</template>
['oops'];
// CURRENT OUTPUT (see CAVEAT)
<template>Hello</template>['oops'];
// or
export default <template>Hello</template>['oops'];
Case: Ambiguous, Dev omits semi, semi: false
Recommendation: Omit semicolon (with caveats as described below)
// INPUT
<template>Hello</template>
['oops']
export default <template>Hello</template>
['oops']
// IDEAL OUTPUT (see ANALYSIS)
<template>Hello</template>
;['oops']
// or
export default <template>Hello</template>
;['oops']
// CURRENT OUTPUT (see CAVEAT)
<template>Hello</template>['oops']
// or
export default <template>Hello</template>['oops']
Case: Ambiguous, Dev includes semi, semi: true
Recommendation: Omit semicolon (with caveats as described below)
// INPUT
<template>Hello</template>;
['oops']
// or
export default <template>Hello</template>;
['oops']
// IDEAL OUTPUT (see ANALYSIS)
<template>Hello</template>
['oops'];
// or
export default <template>Hello</template>
['oops'];
// CURRENT OUTPUT (see CAVEAT)
<template>Hello</template>['oops'];
// or
export default <template>Hello</template>['oops'];
Case: Ambiguous, Dev includes semi, semi: false
Recommendation: Omit semicolon (with caveats as described below)
// INPUT
<template>Hello</template>;
['oops']
// or
export default <template>Hello</template>;
['oops']
// IDEAL OUTPUT (see ANALYSIS)
<template>Hello</template>
;['oops']
// or
export default <template>Hello</template>
;['oops']
// CURRENT OUTPUT (see CAVEAT)
<template>Hello</template>['oops'];
// or
export default <template>Hello</template>['oops'];
Top-Level Class
The template tag can be used as a top-level element in a class as shown below. In this case I recommend omitting the semicolon.
// class-backed-component.gjs
export default class MyComponent extends Component {
<template>Hello</template>
}
For analysis, we’ll look at how Prettier formats the analogous ClassMethod
production, which is generally not followed by a semicolon.
Analysis Details
Case: Unambiguous, semi: true
Recommendation: Omit semicolon
// INPUT
export default class MyComponent extends Component {
<template>Hello</template>
}
// OUTPUT
export default class MyComponent extends Component {
<template>Hello</template>
}
Case: Unambiguous, semi: false
Recommendation: Omit semicolon
// INPUT
export default class MyComponent extends Component {
<template>Hello</template>
}
// OUTPUT
export default class MyComponent extends Component {
<template>Hello</template>
}
Case: Ambiguous, Dev omits semi, semi: true
Recommendation: Omit semicolon
// INPUT
export default class MyComponent extends Component {
<template>Hello</template>
['oops']
}
// OUTPUT
export default class MyComponent extends Component {
<template>Hello</template>
['oops'];
}
Case: Ambiguous, Dev omits semi, semi: false
Recommendation: Omit semicolon
// INPUT
export default class MyComponent extends Component {
<template>Hello</template>
['oops']
}
// OUTPUT
export default class MyComponent extends Component {
<template>Hello</template>
['oops']
}
Case: Ambiguous, Dev includes semi, semi: true
Recommendation: Omit semicolon
// INPUT
export default class MyComponent extends Component {
<template>Hello</template>;
['oops']
}
// OUTPUT
export default class MyComponent extends Component {
<template>Hello</template>
['oops'];
}
Case: Ambiguous, Dev includes semi, semi: false
Recommendation: Omit semicolon
// INPUT
export default class MyComponent extends Component {
<template>Hello</template>;
['oops']
}
// OUTPUT
export default class MyComponent extends Component {
<template>Hello</template>
['oops']
}
Anywhere else as an expression
Besides the top-level module and top-level class positions, the template tag can be used anywhere else as an expression. In this case, I recommend including a semicolon in places where other plain-JavaScript expressions would receive semicolons.
// my-component.gjs
export const MyComponent = <template>Hello</template>;
// ...
For analysis, we’ll look at how Prettier formats the analogous FunctionExpression
production in this position, frequently followed by a semicolon.
Analysis Details
Case: Unambiguous, semi: true
Recommendation: Include semicolon
// INPUT
export const MyComponent = <template>Hello</template>
// OUTPUT
export const MyComponent = <template>Hello</template>;
Case: Unambiguous, semi: false
Recommendation: Omit semicolon
// INPUT
export const MyComponent = <template>Hello</template>
// OUTPUT
export const MyComponent = <template>Hello</template>
Case: Ambiguous, Dev omits semi, semi: true
Recommendation: Omit semicolon, allow Prettier to cuddle the lines (with caveats as described below)
// INPUT
export const MyComponent = <template>Hello</template>
['oops']
// OUTPUT
export const MyComponent = <template>Hello</template>['oops'];
Case: Ambiguous, Dev omits semi, semi: false
Recommendation: Omit semicolon, allow Prettier to cuddle the lines (with caveats as described below)
// INPUT
export const MyComponent = <template>Hello</template>
['oops']
// OUTPUT
export const MyComponent = <template>Hello</template>['oops']
Case: Ambiguous, Dev includes semi, semi: true
Recommendation: Include semicolon (with caveats as described below)
// INPUT
export const MyComponent = <template>Hello</template>;
['oops']
// OUTPUT
export const MyComponent = <template>Hello</template>;
['oops'];
Case: Ambiguous, Dev includes semi, semi: false
Recommendation: Omit semicolon (with caveats as described below)
// INPUT
export const MyComponent = <template>Hello</template>;
['oops']
// OUTPUT
export const MyComponent = <template>Hello</template>
;['oops']
Caveats RE: Ambiguous Expression Edge Cases
In cases where an ambiguous expression follows the template tag, we are limited by the current formatting strategy:
- Run
preprocessEmbeddedTemplates
from ember-template-imports to replace instances of<template>...</template>
with[__GLIMMER_TEMPLATE('...')]
in the file text. - Parse the resulting string with Babel into an ESTree AST.
- Find the
ArrayExpression
nodes corresponding with[__GLIMMER_TEMPLATE('...')]
in the AST and print those using the handlebars Prettier printer while relying on the default Prettier ESTree printer to print the rest of the nodes.
Remember our definition of "Ambiguous Expression": the next token could be the start of a new expression OR the continuation of the current expression. Because Babel generally assumes the latter, current versions of this plugin will generally be "cuddle" ambiguous expressions with the preceeding template tag line--against the recommendations above. (In the current version of ember-template-imports, these ambiguous expressions may even result in a runtime error--with or without the "cuddling".)
Thus, the current output for the ambiguous examples shown above looks like:
export default class MyComponent extends Component {
<template>Hello</template> // omit semi, still works
['oops']
}
<template>Hello</template>['oops'] // cuddle
export default <template>Hello</template>['oops'] // cuddle
export const MyComponent = <template>Hello</template>['oops']; // cuddle
Assuming the cuddling is undesired, the developer can prevent it either by moving the ambiguous expression to a different place in the file or by including a semicolon, which allows the babel parser to parse the lines separately:
<template>Hello</template>; // manually-added semicolon, maintained by Prettier
['oops'];
export default <template>Hello</template>; // manually-added semicolon, maintained by Prettier
['oops'];
export const MyComponent = <template>Hello</template>; // manually-added semicolon, maintained by Prettier
['oops'];
In the future, ember-template-imports may export a parser that exports a "clean" AST, which could be consumed by this plugin:
- Run
tbdParse
from ember-template-imports, which returns a clean AST with nodes forGlimmerExpression
, etc. - Print the
GlimmerExpression
nodes using the handlebars Prettier printer while still relying on the default Prettier ESTree printer to print the rest of the nodes.
In this case, some of the ambiguous expressions may result in a syntax error, in which case our Prettier plugin would error instead of formatting, similar to how Prettier already handles plain JavaScript syntax errors. Other ambiguous expressions may have their "ambiguity" resolved within the parser, allowing them to be printed as the recommendations show above.