@prettier/plugin-php 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -198,7 +198,7 @@ supported PHP version.
198
198
  You set the `phpVersion` to `composer` and this will only use the `composer.json` file to determine the php
199
199
  version, prettier will crash if the PHP cannot be determined.
200
200
 
201
- You can also set the `phpVersion` to a specific version, such as `7.4`, `8.0`, `8.1`, `8.2`, or `8.3`.
201
+ You can also set the `phpVersion` to a specific version, such as `7.4`, `8.0`, `8.1`, `8.2`, `8.3` or `8.4`.
202
202
 
203
203
  **Please Note:** If the phpVersion is not set correctly for your environment, this plugin will produce code that could
204
204
  be incompatible with your PHP runtime. For example, if you are using PHP 7.4, but the plugin is set to PHP 8.3, it will
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prettier/plugin-php",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "description": "Prettier PHP Plugin",
5
5
  "repository": "prettier/prettier-php",
6
6
  "author": "Lucas Azzola <@azz>",
@@ -26,33 +26,33 @@
26
26
  "index.d.ts"
27
27
  ],
28
28
  "dependencies": {
29
- "linguist-languages": "^8.0.0",
30
- "php-parser": "^3.2.5"
29
+ "linguist-languages": "^9.4.0",
30
+ "php-parser": "^3.7.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@babel/preset-env": "^7.27.2",
34
- "@rollup/plugin-alias": "^5.1.0",
35
- "@rollup/plugin-babel": "^6.0.3",
36
- "@rollup/plugin-commonjs": "^25.0.7",
34
+ "@rollup/plugin-alias": "^6.0.0",
35
+ "@rollup/plugin-babel": "^7.0.0",
36
+ "@rollup/plugin-commonjs": "^29.0.2",
37
37
  "@rollup/plugin-inject": "^5.0.5",
38
38
  "@rollup/plugin-json": "^6.1.0",
39
- "@rollup/plugin-node-resolve": "^15.2.1",
40
- "@rollup/plugin-replace": "^5.0.5",
39
+ "@rollup/plugin-node-resolve": "^16.0.3",
40
+ "@rollup/plugin-replace": "^6.0.3",
41
41
  "@rollup/plugin-terser": "^0.4.3",
42
42
  "c8": "^10.1.3",
43
- "cross-env": "^7.0.2",
43
+ "cross-env": "^10.1.0",
44
44
  "eslint": "8.57.0",
45
- "eslint-config-prettier": "9.1.0",
46
- "eslint-plugin-import": "2.29.1",
47
- "eslint-plugin-jest": "27.6.3",
48
- "eslint-plugin-prettier": "5.1.3",
45
+ "eslint-config-prettier": "10.1.8",
46
+ "eslint-plugin-import": "2.32.0",
47
+ "eslint-plugin-jest": "29.15.4",
48
+ "eslint-plugin-prettier": "5.5.5",
49
49
  "eslint-plugin-prettier-doc": "^1.1.0",
50
- "jest": "30.0.3",
51
- "jest-environment-jsdom": "30.0.2",
50
+ "jest": "30.3.0",
51
+ "jest-environment-jsdom": "30.3.0",
52
52
  "jest-light-runner": "^0.7.9",
53
53
  "jest-snapshot-serializer-raw": "^2.0.0",
54
54
  "prettier": "^3.6.1",
55
- "rollup": "^3.28.1",
55
+ "rollup": "^4.60.0",
56
56
  "strip-ansi": "^7.1.0"
57
57
  },
58
58
  "peerDependencies": {
package/src/clean.mjs CHANGED
@@ -101,6 +101,17 @@ function clean(node, newObj) {
101
101
  newObj.name.name = normalizeMagicMethodName(newObj.name.name);
102
102
  }
103
103
 
104
+ // #[A, B] -> #[A] #[B]: the printer gives an attribute whose arguments
105
+ // break a block of its own (PER-CS 12.3), which is the same declaration.
106
+ if (Array.isArray(node.attrGroups) && node.attrGroups.length > 1) {
107
+ newObj.attrGroups = [
108
+ {
109
+ ...newObj.attrGroups[0],
110
+ attrs: newObj.attrGroups.flatMap((attrGroup) => attrGroup.attrs),
111
+ },
112
+ ];
113
+ }
114
+
104
115
  if (node.kind === "noop") {
105
116
  return null;
106
117
  }
package/src/comments.mjs CHANGED
@@ -62,6 +62,7 @@ function handleOwnLineComment(comment, text, options) {
62
62
  options
63
63
  ) ||
64
64
  handleTryComments(enclosingNode, followingNode, comment) ||
65
+ handleClassMemberStatementComments(enclosingNode, followingNode, comment) ||
65
66
  handleClassComments(enclosingNode, followingNode, comment) ||
66
67
  handleFunctionParameter(
67
68
  text,
@@ -201,6 +202,7 @@ function handleRemainingComment(comment, text, options) {
201
202
  handleGoto(enclosingNode, comment) ||
202
203
  handleHalt(precedingNode, enclosingNode, followingNode, comment) ||
203
204
  handleBreakAndContinueStatementComments(enclosingNode, comment) ||
205
+ handleClassMemberStatementComments(enclosingNode, followingNode, comment) ||
204
206
  handleInlineComments(
205
207
  enclosingNode,
206
208
  precedingNode,
@@ -433,6 +435,32 @@ function handleTraitUseComments(enclosingNode, followingNode, comment) {
433
435
  return false;
434
436
  }
435
437
 
438
+ function handleClassMemberStatementComments(
439
+ enclosingNode,
440
+ followingNode,
441
+ comment
442
+ ) {
443
+ if (
444
+ enclosingNode &&
445
+ enclosingNode.kind === "propertystatement" &&
446
+ enclosingNode.properties?.includes(followingNode)
447
+ ) {
448
+ addLeadingComment(enclosingNode, comment);
449
+ return true;
450
+ }
451
+
452
+ if (
453
+ enclosingNode &&
454
+ enclosingNode.kind === "classconstant" &&
455
+ enclosingNode.constants?.includes(followingNode)
456
+ ) {
457
+ addLeadingComment(enclosingNode, comment);
458
+ return true;
459
+ }
460
+
461
+ return false;
462
+ }
463
+
436
464
  function handleClassComments(enclosingNode, followingNode, comment) {
437
465
  if (
438
466
  enclosingNode &&
@@ -564,8 +592,8 @@ function handleFunctionParameter(
564
592
  return false;
565
593
  }
566
594
  if (
567
- precedingNode.kind === "typereference" &&
568
- followingNode.kind === "identifier"
595
+ precedingNode?.kind === "typereference" &&
596
+ followingNode?.kind === "identifier"
569
597
  ) {
570
598
  addTrailingComment(precedingNode, comment);
571
599
  return true;
@@ -907,6 +935,29 @@ function getCommentChildNodes(node) {
907
935
  node.what.__parent_new_arguments = [...node.arguments];
908
936
  return [node.what];
909
937
  }
938
+
939
+ if (node.attrGroups && node.attrGroups.length > 0) {
940
+ if (node.kind === "method" || node.kind === "function") {
941
+ // Keep every child (in source order): dropping `body` makes comments
942
+ // inside the body resolve against the return type, dropping `name`
943
+ // makes a comment after the attribute look like it precedes the body.
944
+ return [
945
+ ...node.attrGroups,
946
+ ...(node.name && typeof node.name === "object" ? [node.name] : []),
947
+ ...node.arguments,
948
+ ...(node.type ? [node.type] : []),
949
+ ...(node.body ? [node.body] : []),
950
+ ];
951
+ }
952
+
953
+ if (node.kind === "classconstant") {
954
+ return [...node.attrGroups, ...node.constants];
955
+ }
956
+
957
+ if (node.kind === "enumcase") {
958
+ return node.attrGroups;
959
+ }
960
+ }
910
961
  }
911
962
 
912
963
  function canAttachComment(node) {
@@ -142,7 +142,8 @@ function needsParens(path, options) {
142
142
  return false;
143
143
  }
144
144
  }
145
- case "yield": {
145
+ case "yield":
146
+ case "yieldfrom": {
146
147
  switch (parent.kind) {
147
148
  case "propertylookup":
148
149
  case "nullsafepropertylookup":
@@ -154,8 +155,11 @@ function needsParens(path, options) {
154
155
  case "retif":
155
156
  return key === "test";
156
157
 
158
+ case "assign":
159
+ return node.kind === "yield" && !!(node.key || node.value);
160
+
157
161
  default:
158
- return !!(node.key || node.value);
162
+ return node.kind === "yieldfrom" || !!(node.key || node.value);
159
163
  }
160
164
  }
161
165
  case "assign": {
@@ -166,7 +170,7 @@ function needsParens(path, options) {
166
170
  return false;
167
171
  } else if (parent.kind === "assign") {
168
172
  return false;
169
- } else if (parent.kind === "static") {
173
+ } else if (parent.kind === "staticvariable") {
170
174
  return false;
171
175
  } else if (
172
176
  ["if", "do", "while", "foreach", "switch"].includes(parent.kind)
@@ -188,7 +192,12 @@ function needsParens(path, options) {
188
192
  case "bin":
189
193
  case "retif":
190
194
  if (key === "test" && !parent.trueExpr) {
191
- return false;
195
+ // A full ternary in the test position of a shorthand ternary
196
+ // (`?:`) needs parens: `1 ? 2 : 3 ?: 4` is a syntax error in
197
+ // PHP 8. Shorthand ternaries chain left-associatively, so they
198
+ // don't need parens here.
199
+ // https://github.com/prettier/plugin-php/issues/2115
200
+ return !!node.trueExpr;
192
201
  }
193
202
 
194
203
  return true;
@@ -215,7 +224,7 @@ function needsParens(path, options) {
215
224
  default:
216
225
  return false;
217
226
  }
218
- case "silence":
227
+ case "silent":
219
228
  case "cast":
220
229
  // TODO: bug https://github.com/glayzzle/php-parser/issues/172
221
230
  return node.parenthesizedExpression;
package/src/options.mjs CHANGED
@@ -7,7 +7,7 @@ const CATEGORY_PHP = "PHP";
7
7
  const SUPPORTED_PHP_VERSIONS = [
8
8
  5.0, 5.1, 5.2, 5.3, 5.4, 5.5, 5.6,
9
9
  7.0, 7.1, 7.2, 7.3, 7.4,
10
- 8.0, 8.1, 8.2, 8.3, 8.4,
10
+ 8.0, 8.1, 8.2, 8.3, 8.4, 8.5,
11
11
  ];
12
12
 
13
13
  export const LATEST_SUPPORTED_PHP_VERSION = Math.max(...SUPPORTED_PHP_VERSIONS);
package/src/printer.mjs CHANGED
@@ -50,6 +50,7 @@ const {
50
50
  dedent,
51
51
  ifBreak,
52
52
  hardline,
53
+ hardlineWithoutBreakParent,
53
54
  softline,
54
55
  literalline,
55
56
  align,
@@ -741,6 +742,32 @@ function shouldInlineLogicalExpression(node) {
741
742
  // precedence level and the AST is structured based on precedence
742
743
  // level, things are naturally broken up correctly, i.e. `&&` is
743
744
  // broken before `+`.
745
+ function printPipeChain(path, print, trailingSemicolon = false) {
746
+ const elements = [];
747
+
748
+ function collect() {
749
+ const { node } = path;
750
+ if (node.kind === "bin" && node.type === "|>") {
751
+ elements.push(path.call(print, "left"));
752
+ path.call(collect, "right");
753
+ } else {
754
+ elements.push(print());
755
+ }
756
+ }
757
+
758
+ collect();
759
+
760
+ const [first, ...rest] = elements;
761
+ const semicolon = trailingSemicolon
762
+ ? ifBreak([hardlineWithoutBreakParent, ";"], ";")
763
+ : "";
764
+ return group([
765
+ first,
766
+ indent(rest.flatMap((el) => [line, "|> ", el])),
767
+ semicolon,
768
+ ]);
769
+ }
770
+
744
771
  function printBinaryExpression(
745
772
  path,
746
773
  print,
@@ -1172,31 +1199,16 @@ function printAttrs(path, options, print, { inline = false } = {}) {
1172
1199
  if (!path.node.attrGroups) {
1173
1200
  return [];
1174
1201
  }
1175
- path.each(() => {
1176
- const attrGroup = ["#["];
1202
+ path.each((attrGroupPath) => {
1177
1203
  if (!inline && allAttrs.length > 0) {
1178
1204
  allAttrs.push(hardline);
1179
1205
  }
1180
- attrGroup.push(softline);
1181
- path.each(() => {
1182
- const attrNode = path.node;
1183
- if (attrGroup.length > 2) {
1184
- attrGroup.push(",", line);
1185
- }
1186
- const attrStmt = [attrNode.name];
1187
- if (attrNode.args.length > 0) {
1188
- attrStmt.push(printArgumentsList(path, options, print, "args"));
1189
- }
1190
- attrGroup.push(group(attrStmt));
1191
- }, "attrs");
1192
1206
  allAttrs.push(
1193
- group([
1194
- indent(attrGroup),
1195
- ifBreak(shouldPrintComma(options, 8.0) ? "," : ""),
1196
- softline,
1197
- "]",
1198
- inline ? ifBreak(softline, " ") : "",
1199
- ])
1207
+ printAllComments(
1208
+ attrGroupPath,
1209
+ () => printAttrGroup(attrGroupPath, options, print, { inline }),
1210
+ options
1211
+ )
1200
1212
  );
1201
1213
  }, "attrGroups");
1202
1214
  if (allAttrs.length === 0) {
@@ -1205,6 +1217,40 @@ function printAttrs(path, options, print, { inline = false } = {}) {
1205
1217
  return [...allAttrs, inline ? "" : hardline];
1206
1218
  }
1207
1219
 
1220
+ // PER-CS 12.3: an attribute whose argument list is split over several lines
1221
+ // must be the only one in its `#[...]` block, and its arguments follow the
1222
+ // multiline function call rules (`#[Name(` ... `)]`). So a block is printed on
1223
+ // one line while it fits; otherwise each attribute gets a block of its own and
1224
+ // only the argument lists break.
1225
+ function printAttrGroup(path, options, print, { inline = false } = {}) {
1226
+ const printedAttrs = path.map(({ node: attrNode }) => {
1227
+ const attrStmt = [attrNode.name];
1228
+ if (attrNode.args.length > 0) {
1229
+ attrStmt.push(printArgumentsList(path, options, print, "args"));
1230
+ }
1231
+ return group(attrStmt);
1232
+ }, "attrs");
1233
+ const separator = inline ? ifBreak(softline, " ") : "";
1234
+
1235
+ if (printedAttrs.length === 1) {
1236
+ return group(["#[", printedAttrs[0], "]", separator]);
1237
+ }
1238
+
1239
+ const printedOneBlock = ["#[", join(", ", printedAttrs), "]", separator];
1240
+ const printedOneBlockEach = group(
1241
+ [
1242
+ join(
1243
+ inline ? line : hardline,
1244
+ printedAttrs.map((attr) => ["#[", attr, "]"])
1245
+ ),
1246
+ inline ? line : "",
1247
+ ],
1248
+ { shouldBreak: true }
1249
+ );
1250
+
1251
+ return conditionalGroup([printedOneBlock, printedOneBlockEach]);
1252
+ }
1253
+
1208
1254
  function printClass(path, options, print) {
1209
1255
  const { node } = path;
1210
1256
  const isAnonymousClass = node.kind === "class" && node.isAnonymous;
@@ -1219,7 +1265,7 @@ function printClass(path, options, print) {
1219
1265
  declaration.push("abstract ");
1220
1266
  }
1221
1267
 
1222
- if (node.isReadonly) {
1268
+ if (node.isReadonly && !isAnonymousClass) {
1223
1269
  declaration.push("readonly ");
1224
1270
  }
1225
1271
 
@@ -1431,6 +1477,38 @@ function printFunction(path, options, print) {
1431
1477
  ];
1432
1478
  }
1433
1479
 
1480
+ // PHP 8.4 property hooks (`{ get => ...; set(...) { ... } }`) on a property or a
1481
+ // promoted constructor parameter. PER-CS puts the opening brace on the
1482
+ // declaration line and one hook per line, preserving single blank lines.
1483
+ function printPropertyHooks(path, options, print) {
1484
+ const { node } = path;
1485
+
1486
+ if (!node.hooks || node.hooks.length === 0) {
1487
+ return "";
1488
+ }
1489
+
1490
+ // Interface / abstract declarations (`{ get; set; }`) stay on the property's line.
1491
+ if (node.hooks.every((hook) => !hook.body)) {
1492
+ return [" { ", join(" ", path.map(print, "hooks")), " }"];
1493
+ }
1494
+
1495
+ const printedHooks = path.map(({ node: hook, isLast }) => {
1496
+ const parts = [print()];
1497
+
1498
+ if (!isLast) {
1499
+ parts.push(hardline);
1500
+
1501
+ if (isNextLineEmpty(options.originalText, locEnd(hook))) {
1502
+ parts.push(hardline);
1503
+ }
1504
+ }
1505
+
1506
+ return parts;
1507
+ }, "hooks");
1508
+
1509
+ return [" {", indent([hardline, ...printedHooks]), hardline, "}"];
1510
+ }
1511
+
1434
1512
  function printBodyControlStructure(
1435
1513
  path,
1436
1514
  options,
@@ -1524,6 +1602,7 @@ function printAssignmentRight(
1524
1602
 
1525
1603
  const canBreak =
1526
1604
  (pureRightNode.kind === "bin" &&
1605
+ pureRightNode.type !== "|>" &&
1527
1606
  !shouldInlineLogicalExpression(pureRightNode)) ||
1528
1607
  (pureRightNode.kind === "retif" &&
1529
1608
  ((!pureRightNode.trueExpr &&
@@ -1738,14 +1817,13 @@ function printNode(path, options, print) {
1738
1817
  node.parenthesizedExpression ? ")" : "",
1739
1818
  ];
1740
1819
  case "parameter": {
1741
- let promoted = "";
1742
- if (node.flags === 1) {
1743
- promoted = "public ";
1744
- } else if (node.flags === 2) {
1745
- promoted = "protected ";
1746
- } else if (node.flags === 4) {
1747
- promoted = "private ";
1748
- }
1820
+ const visibilities = { 1: "public", 2: "protected", 4: "private" };
1821
+ // promoted constructor property: `public`, and/or the asymmetric set
1822
+ // visibility `private(set)` (PHP 8.4)
1823
+ const promoted = [
1824
+ node.flags ? [visibilities[node.flags], " "] : "",
1825
+ node.flagsSet ? [visibilities[node.flagsSet], "(set) "] : "",
1826
+ ];
1749
1827
  const name = [
1750
1828
  ...printAttrs(path, options, print, { inline: true }),
1751
1829
  promoted,
@@ -1757,6 +1835,7 @@ function printNode(path, options, print) {
1757
1835
  "$",
1758
1836
  print("name"),
1759
1837
  ];
1838
+ const hooks = printPropertyHooks(path, options, print);
1760
1839
 
1761
1840
  if (node.value) {
1762
1841
  return group([
@@ -1774,10 +1853,11 @@ function printNode(path, options, print) {
1774
1853
  false,
1775
1854
  options
1776
1855
  ),
1856
+ hooks,
1777
1857
  ]);
1778
1858
  }
1779
1859
 
1780
- return name;
1860
+ return [name, hooks];
1781
1861
  }
1782
1862
  case "variadic":
1783
1863
  return ["...", print("what")];
@@ -1799,7 +1879,41 @@ function printNode(path, options, print) {
1799
1879
  ),
1800
1880
  ]
1801
1881
  : "",
1882
+ printPropertyHooks(path, options, print),
1802
1883
  ]);
1884
+ case "propertyhook": {
1885
+ const declaration = [
1886
+ ...printAttrs(path, options, print, { inline: true }),
1887
+ node.isFinal ? "final " : "",
1888
+ node.byref ? "&" : "",
1889
+ node.name,
1890
+ node.parameter ? ["(", print("parameter"), ")"] : "",
1891
+ ];
1892
+
1893
+ // `get;` / `set;` in an interface or abstract class
1894
+ if (!node.body) {
1895
+ return [...declaration, ";"];
1896
+ }
1897
+
1898
+ // `get => expr;`
1899
+ if (node.body.kind !== "block") {
1900
+ return group([
1901
+ ...declaration,
1902
+ " =>",
1903
+ printAssignmentRight(node, node.body, print("body"), false, options),
1904
+ ";",
1905
+ ]);
1906
+ }
1907
+
1908
+ // `get { ... }` - like a closure, the brace stays on the hook's line
1909
+ return [
1910
+ ...declaration,
1911
+ " {",
1912
+ indent([hasEmptyBody(path) ? "" : hardline, print("body")]),
1913
+ hasEmptyBody(path) ? "" : hardline,
1914
+ "}",
1915
+ ];
1916
+ }
1803
1917
  case "propertystatement": {
1804
1918
  const attrs = [];
1805
1919
  path.each(() => {
@@ -1822,10 +1936,18 @@ function printNode(path, options, print) {
1822
1936
 
1823
1937
  return group([
1824
1938
  ...attrs,
1939
+ node.isAbstract ? "abstract " : "",
1940
+ node.isFinal ? "final " : "",
1825
1941
  hasVisibility
1826
1942
  ? [node.visibility === null ? "var" : node.visibility, ""]
1827
1943
  : "",
1828
- node.isStatic ? [hasVisibility ? " " : "", "static"] : "",
1944
+ // asymmetric visibility: `public private(set)`
1945
+ node.visibilitySet
1946
+ ? [hasVisibility ? " " : "", node.visibilitySet, "(set)"]
1947
+ : "",
1948
+ node.isStatic
1949
+ ? [hasVisibility || node.visibilitySet ? " " : "", "static"]
1950
+ : "",
1829
1951
  firstProperty ? [" ", firstProperty] : "",
1830
1952
  indent(
1831
1953
  printed.slice(1).map((p) => [",", hasValue ? hardline : line, p])
@@ -2094,7 +2216,7 @@ function printNode(path, options, print) {
2094
2216
  () => printAttrs(path, options, print, { inline: true }),
2095
2217
  "what"
2096
2218
  ),
2097
- "class",
2219
+ node.what.isReadonly ? "readonly class" : "class",
2098
2220
  node.arguments.length > 0
2099
2221
  ? [" ", printArgumentsList(path, options, print)]
2100
2222
  : "",
@@ -2114,11 +2236,64 @@ function printNode(path, options, print) {
2114
2236
 
2115
2237
  return parts;
2116
2238
  }
2117
- case "clone":
2239
+ case "clone": {
2240
+ if (!node.properties) {
2241
+ return [
2242
+ "clone ",
2243
+ node.what.comments ? indent(print("what")) : print("what"),
2244
+ ];
2245
+ }
2246
+
2247
+ // PHP 8.5 clone with: `clone($obj, ['prop' => $value])`, laid out like
2248
+ // a call with two arguments (see printArgumentsList). No trailing comma:
2249
+ // php-parser does not accept one after the properties expression.
2250
+ const printedWhat = print("what");
2251
+ const printedProperties = print("properties");
2252
+ const printedArguments = [printedWhat, ",", line, printedProperties];
2253
+ const allArgsBrokenOut = () =>
2254
+ group(["clone(", indent([line, ...printedArguments]), line, ")"], {
2255
+ shouldBreak: true,
2256
+ });
2257
+
2258
+ if (!shouldGroupLastArg([node.what, node.properties])) {
2259
+ return group(
2260
+ ["clone(", indent([softline, ...printedArguments]), softline, ")"],
2261
+ {
2262
+ shouldBreak: willBreak(printedWhat) || willBreak(printedProperties),
2263
+ }
2264
+ );
2265
+ }
2266
+
2267
+ const somePrintedArgumentsWillBreak =
2268
+ willBreak(printedWhat) || willBreak(printedProperties);
2269
+ const simpleConcat = [
2270
+ "clone(",
2271
+ printedWhat,
2272
+ ", ",
2273
+ printedProperties,
2274
+ ")",
2275
+ ];
2276
+
2118
2277
  return [
2119
- "clone ",
2120
- node.what.comments ? indent(print("what")) : print("what"),
2278
+ somePrintedArgumentsWillBreak ? breakParent : "",
2279
+ conditionalGroup(
2280
+ [
2281
+ somePrintedArgumentsWillBreak
2282
+ ? ifBreak(allArgsBrokenOut(), simpleConcat)
2283
+ : simpleConcat,
2284
+ [
2285
+ "clone(",
2286
+ printedWhat,
2287
+ ", ",
2288
+ group(printedProperties, { shouldBreak: true }),
2289
+ ")",
2290
+ ],
2291
+ allArgsBrokenOut(),
2292
+ ],
2293
+ { shouldBreak: willBreak(printedWhat) }
2294
+ ),
2121
2295
  ];
2296
+ }
2122
2297
  case "propertylookup":
2123
2298
  case "nullsafepropertylookup":
2124
2299
  case "staticlookup":
@@ -2278,6 +2453,10 @@ function printNode(path, options, print) {
2278
2453
  parent.curly) ||
2279
2454
  (parentParent &&
2280
2455
  parent.kind === "offsetlookup" &&
2456
+ // Only the base variable of the lookup follows the `${` printed
2457
+ // by the encapsedpart; a variable in the offset position is a
2458
+ // nested variable and keeps its own `$`.
2459
+ parent.what === node &&
2281
2460
  parentParent.kind === "encapsedpart" &&
2282
2461
  parentParent.syntax === "simple" &&
2283
2462
  parentParent.curly)
@@ -2487,6 +2666,16 @@ function printNode(path, options, print) {
2487
2666
  );
2488
2667
  }
2489
2668
  case "bin": {
2669
+ if (node.type === "|>") {
2670
+ const { parent, grandparent } = path;
2671
+ const isStatementLevel =
2672
+ parent.kind === "expressionstatement" ||
2673
+ (parent.kind === "assign" &&
2674
+ grandparent &&
2675
+ grandparent.kind === "expressionstatement");
2676
+ return printPipeChain(path, print, isStatementLevel);
2677
+ }
2678
+
2490
2679
  const { parent, grandparent: parentParent } = path;
2491
2680
  const isInsideParenthesis =
2492
2681
  node !== parent.body &&
@@ -2871,6 +3060,7 @@ function printNode(path, options, print) {
2871
3060
 
2872
3061
  case "enumcase":
2873
3062
  return group([
3063
+ ...printAttrs(path, options, print),
2874
3064
  "case ",
2875
3065
  print("name"),
2876
3066
  node.value
package/src/util.mjs CHANGED
@@ -41,6 +41,7 @@ const PRECEDENCE = new Map(
41
41
  "<<=",
42
42
  ">>=",
43
43
  ],
44
+ ["|>"],
44
45
  ["??"],
45
46
  ["||"],
46
47
  ["&&"],
@@ -70,7 +71,7 @@ const bitshiftOperators = [">>", "<<"];
70
71
 
71
72
  function isBitwiseOperator(operator) {
72
73
  return (
73
- !!bitshiftOperators[operator] ||
74
+ bitshiftOperators.includes(operator) ||
74
75
  operator === "|" ||
75
76
  operator === "^" ||
76
77
  operator === "&"
@@ -353,12 +354,30 @@ function lineShouldEndWithSemicolon(path) {
353
354
  if (node.kind === "method" && node.isAbstract) {
354
355
  return true;
355
356
  }
357
+ // a hooked property ends with its closing brace, not a semicolon
358
+ if (
359
+ node.kind === "propertystatement" &&
360
+ node.properties.some((property) => property.hooks?.length > 0)
361
+ ) {
362
+ return false;
363
+ }
356
364
  if (node.kind === "method") {
357
365
  const { parent } = path;
358
366
  if (parent && parent.kind === "interface") {
359
367
  return true;
360
368
  }
361
369
  }
370
+ if (node.kind === "expressionstatement") {
371
+ const expr = node.expression;
372
+ const isPipeChain =
373
+ (expr.kind === "bin" && expr.type === "|>") ||
374
+ (expr.kind === "assign" &&
375
+ expr.right.kind === "bin" &&
376
+ expr.right.type === "|>");
377
+ if (isPipeChain) {
378
+ return false;
379
+ }
380
+ }
362
381
  return [
363
382
  "expressionstatement",
364
383
  "do",
@@ -678,7 +697,7 @@ function isSimpleCallArgument(node, depth = 2) {
678
697
 
679
698
  if (isCallLikeExpression(node)) {
680
699
  const parts = getChainParts(node);
681
- parts.unshift();
700
+ parts.shift();
682
701
 
683
702
  return (
684
703
  parts.length <= depth &&