gitKrystan/prettier-plugin-ember-template-tag

RFC: Semicolon constraints and opinions

gitKrystan opened this issue · 0 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.js

<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>

ANALYSIS

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>

ANALYSIS

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'];

ANALYSIS

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']

ANALYSIS

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'];

ANALYSIS

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'];

ANALYSIS

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.js

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>
}

ANALYSIS

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>
}

ANALYSIS

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'];
}

ANALYSIS

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']
}

ANALYSIS

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'];
}

ANALYSIS

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']
}

ANALYSIS

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.js

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>;

ANALYSIS

Case: Unambiguous, semi: false

Recommendation: Omit semicolon

// INPUT

export const MyComponent = <template>Hello</template>

// OUTPUT

export const MyComponent = <template>Hello</template>

ANALYSIS

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'];

ANALYSIS

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']

ANALYSIS

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'];

ANALYSIS

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']

ANALYSIS

Caveats RE: Ambiguous Expression Edge Cases

In cases where an ambiguous expression follows the template tag, we are limited by the current formatting strategy:

  1. Run preprocessEmbeddedTemplates from ember-template-imports to replace instances of <template>...</template> with [__GLIMMER_TEMPLATE('...')] in the file text.
  2. Parse the resulting string with Babel into an ESTree AST.
  3. 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:

  1. Run tbdParse from ember-template-imports, which returns a clean AST with nodes for GlimmerExpression, etc.
  2. 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.