Repository: prettier/prettier (JavaScript)

Issue:
Handlebars / Glimmer: Square brackets removed from array index - causes SyntaxError

**Prettier 2.8.3**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuExgAc4XQGzgOgG0BGAXXygEMBbOAXzoB0o1Ns8iyGQAaEbGAEtoAZ2ShKAJ0kQA7gAUpCMSko5ZlAJ5i+AI0mUwAazgwAyjTgAZQVDjIAZmpFw9B46bPpDtgObIYSQBXVxA4al04ABMo6KtKKF8gyl84ADEISWpKGCFE5BBKIJgIXhAACxhqHAB1csF4EW8wODNlBsEANwbNArARHRBbF0kYeQNfbMdnUIArEQAPMz88AEUgiHhpnBc+b0kRgt8cQWpaSTL0SVsYGsEomHLkAA4ABj2ZFxqDdAKruBGnXsfAAjht4ONsCpCiIALR2aLRMqSOBgwQo8YpKZIJw7UIuaiCALBfErOAAQVy110xTg8jgkhsdm2uxAIjJ6029hxMz4MEoujuDyeSAATHyDIITokAMIQM6UAoAgCsZSCLgAKgKVLjWZ0QgBJKCxWBmMDXdAwcnGswwTR4Fn0OhAA)
<!-- prettier-ignore -->
```sh
--parser glimmer
```

**Input:**
<!-- prettier-ignore -->
```hbs
{{people.[1].name}}
{{people.[1]}}
```

**Output:**
<!-- prettier-ignore -->
```hbs
{{people.1.name}}
{{people.1}}
```

When parsed, this output produces a syntax error on the second line

```
SyntaxError: Parse error on line 2:
...e.1.name}}{{people.1}}
----------------------^
Expecting 'ID', got 'NUMBER' (2:10)
  1 | {{people.1.name}}
> 2 | {{people.1}}
```

**Expected behavior:**

Prettier should only remove square brackets when the array index is followed by another property.

The correct output for the given input would be:

```hbs
{{people.1.name}}
{{people.[1]}}
```

Fix square brackets removed from end of array
## Description

Fixes issue #14207 and handles literal segments.

## Checklist

- [ ] I’ve added tests to confirm my change works.
- [ ] (If changing the API or CLI) I’ve documented the changes I’ve made (in the `docs/` directory).
- [ ] (If the change is user-facing) I’ve added my changes to `changelog_unreleased/*/XXXX.md` file following `changelog_unreleased/TEMPLATE.md`.
- [x] I’ve read the [contributing guidelines](https://github.com/prettier/prettier/blob/main/CONTRIBUTING.md).

**✨[Try the playground for this PR](https://prettier.io/playground-redirect)✨**



Hints:
Hi guys! Can I work on this?

I assume we need to change this file (Not sure which function we need to change yet)
https://github.com/prettier/prettier/blob/main/src/language-handlebars/printer-glimmer.js
@shogohida Still interested? Related code should be here https://github.com/prettier/prettier/blob/ddf3b43c33e2e98f6413b5232ad623876d96738e/src/language-handlebars/printer-glimmer.js#L396-L398

According to [this](https://handlebarsjs.com/guide/expressions.html#literal-segments), there are other cases except numbers, need put in `[]`.
@fisker 
Yeah, thanks for your comment!

I checked the link you provided. Is this the case for this issue?
![スクリーンショット 2023-03-25 22 10 31](https://user-images.githubusercontent.com/10365357/227719181-7f8fcae7-b074-41ad-9cfc-b50335f011de.png)

I'm not sure how I should change the code yet... Can you give me more advice?
Hi, I haven't seen any updates on this so I was hoping I could start working on it. I've gotten a bit of progress with it.
LGTM!
The example says not only the last part need add `[]` https://handlebarsjs.com/guide/expressions.html#literal-segments

```
{{!-- wrong: {{array.0.item}} --}}
correct: array.[0].item: {{array.[0].item}}
```

Let's check every part to be safe?

Patch:
diff --git a/src/language-handlebars/printer-glimmer.js b/src/language-handlebars/printer-glimmer.js
--- a/src/language-handlebars/printer-glimmer.js
+++ b/src/language-handlebars/printer-glimmer.js
@@ -366,7 +366,7 @@ function print(path, options, print) {
       ];
     }
     case "PathExpression":
-      return node.original;
+      return printPathExpression(node);
 
     case "BooleanLiteral":
       return String(node.value);
@@ -764,6 +764,42 @@ function printBlockParams(node) {
   return ["as |", node.blockParams.join(" "), "|"];
 }
 
+// https://handlebarsjs.com/guide/expressions.html#literal-segments
+const PATH_EXPRESSION_FORBIDDEN_CHARACTERS = new Set(
+  "!\"#%&'()*+,./;<=>@[\\]^`{|}~",
+);
+const PATH_EXPRESSION_FORBIDDEN_IN_FIRST_PART = new Set([
+  "true",
+  "false",
+  "null",
+  "undefined",
+]);
+const isPathExpressionPartNeedBrackets = (part, index) =>
+  (index !== 0 && PATH_EXPRESSION_FORBIDDEN_IN_FIRST_PART.has(part)) ||
+  /\s/.test(part) ||
+  /^\d/.test(part) ||
+  Array.prototype.some.call(part, (character) =>
+    PATH_EXPRESSION_FORBIDDEN_CHARACTERS.has(character),
+  );
+function printPathExpression(node) {
+  if (node.data || (node.parts.length === 1 && node.original.includes("/"))) {
+    // check if node has data, or
+    // check if node is a legacy path expression (and leave it alone)
+    return node.original;
+  }
+
+  let { parts } = node;
+  if (node.this) {
+    parts = ["this", ...parts];
+  }
+
+  return parts
+    .map((part, index) =>
+      isPathExpressionPartNeedBrackets(part, index) ? `[${part}]` : part,
+    )
+    .join(".");
+}
+
 const printer = {
   print,
   massageAstNode: clean,


Test patch:
diff --git a/tests/format/handlebars/path-expressions/__snapshots__/jsfmt.spec.js.snap b/tests/format/handlebars/path-expressions/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
--- /dev/null
+++ b/tests/format/handlebars/path-expressions/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,119 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`keyword-expressions.hbs format 1`] = `
+====================================options=====================================
+parsers: ["glimmer"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+<div>
+  {{true.foo}}
+  {{foo.[true]}}
+  {{false.foo}}
+  {{foo.[false]}}
+  {{null.foo}}
+  {{foo.[null]}}
+  {{undefined.foo}}
+  {{foo.[undefined]}}
+</div>
+
+=====================================output=====================================
+<div>
+  {{true.foo}}
+  {{foo.[true]}}
+  {{false.foo}}
+  {{foo.[false]}}
+  {{null.foo}}
+  {{foo.[null]}}
+  {{undefined.foo}}
+  {{foo.[undefined]}}
+</div>
+================================================================================
+`;
+
+exports[`literal-expressions.hbs format 1`] = `
+====================================options=====================================
+parsers: ["glimmer"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+<div>
+  {{array.[true]}}
+  {{[dot.]}}
+  {{array.2.[@#].[1]}}
+  {{array.2.[a b]}}
+  {{this.test}}
+  {{#if @foo}}
+  {{/if}}
+  <!-- legacy path format, which glimmer does not parse - we don't want to wrap with brackets -->
+  {{foo/bar/baz ab=cd}}
+</div>
+
+=====================================output=====================================
+<div>
+  {{array.[true]}}
+  {{[dot.]}}
+  {{array.[2].[@#].[1]}}
+  {{array.[2].[a b]}}
+  {{this.test}}
+  {{#if @foo}}{{/if}}
+  <!-- legacy path format, which glimmer does not parse - we don't want to wrap with brackets -->
+  {{foo/bar/baz ab=cd}}
+</div>
+================================================================================
+`;
+
+exports[`numeric-expressions.hbs format 1`] = `
+====================================options=====================================
+parsers: ["glimmer"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+<div>
+  {{people.[1].name}}
+  {{people.[1]}}
+  {{array.[1].barray.[13]}}
+  {{array.[1234]}}
+  {{array.[123].[item-class]}}
+  {{array.[12].item}}
+  {{people.[1a]}}
+</div>
+
+<ul class="people_list">
+  {{#each people}}
+  <li>{{array.[2]}}</li>
+  {{/each}}
+</ul>
+
+<div>
+  {{#if @foo}}
+  {{bar.[1]}}
+  {{bar.1.name}}
+  {{/if}}
+</div>
+
+=====================================output=====================================
+<div>
+  {{people.[1].name}}
+  {{people.[1]}}
+  {{array.[1].barray.[13]}}
+  {{array.[1234]}}
+  {{array.[123].item-class}}
+  {{array.[12].item}}
+  {{people.[1a]}}
+</div>
+
+<ul class="people_list">
+  {{#each people}}
+    <li>{{array.[2]}}</li>
+  {{/each}}
+</ul>
+
+<div>
+  {{#if @foo}}
+    {{bar.[1]}}
+    {{bar.[1].name}}
+  {{/if}}
+</div>
+================================================================================
+`;
diff --git a/tests/format/handlebars/path-expressions/jsfmt.spec.js b/tests/format/handlebars/path-expressions/jsfmt.spec.js
new file mode 100644
--- /dev/null
+++ b/tests/format/handlebars/path-expressions/jsfmt.spec.js
@@ -0,0 +1 @@
+run_spec(import.meta, ["glimmer"]);
diff --git a/tests/format/handlebars/path-expressions/keyword-expressions.hbs b/tests/format/handlebars/path-expressions/keyword-expressions.hbs
new file mode 100644
--- /dev/null
+++ b/tests/format/handlebars/path-expressions/keyword-expressions.hbs
@@ -0,0 +1,10 @@
+<div>
+  {{true.foo}}
+  {{foo.[true]}}
+  {{false.foo}}
+  {{foo.[false]}}
+  {{null.foo}}
+  {{foo.[null]}}
+  {{undefined.foo}}
+  {{foo.[undefined]}}
+</div>
diff --git a/tests/format/handlebars/path-expressions/literal-expressions.hbs b/tests/format/handlebars/path-expressions/literal-expressions.hbs
new file mode 100644
--- /dev/null
+++ b/tests/format/handlebars/path-expressions/literal-expressions.hbs
@@ -0,0 +1,11 @@
+<div>
+  {{array.[true]}}
+  {{[dot.]}}
+  {{array.2.[@#].[1]}}
+  {{array.2.[a b]}}
+  {{this.test}}
+  {{#if @foo}}
+  {{/if}}
+  <!-- legacy path format, which glimmer does not parse - we don't want to wrap with brackets -->
+  {{foo/bar/baz ab=cd}}
+</div>
diff --git a/tests/format/handlebars/path-expressions/numeric-expressions.hbs b/tests/format/handlebars/path-expressions/numeric-expressions.hbs
new file mode 100644
--- /dev/null
+++ b/tests/format/handlebars/path-expressions/numeric-expressions.hbs
@@ -0,0 +1,22 @@
+<div>
+  {{people.[1].name}}
+  {{people.[1]}}
+  {{array.[1].barray.[13]}}
+  {{array.[1234]}}
+  {{array.[123].[item-class]}}
+  {{array.[12].item}}
+  {{people.[1a]}}
+</div>
+
+<ul class="people_list">
+  {{#each people}}
+  <li>{{array.[2]}}</li>
+  {{/each}}
+</ul>
+
+<div>
+  {{#if @foo}}
+  {{bar.[1]}}
+  {{bar.1.name}}
+  {{/if}}
+</div>