@prettier/plugin-php 0.25.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prettier/plugin-php",
3
- "version": "0.25.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,12 +26,12 @@
26
26
  "index.d.ts"
27
27
  ],
28
28
  "dependencies": {
29
- "linguist-languages": "^8.0.0",
30
- "php-parser": "^3.4.0"
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",
34
+ "@rollup/plugin-alias": "^6.0.0",
35
35
  "@rollup/plugin-babel": "^7.0.0",
36
36
  "@rollup/plugin-commonjs": "^29.0.2",
37
37
  "@rollup/plugin-inject": "^5.0.5",
@@ -40,11 +40,11 @@
40
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",
45
+ "eslint-config-prettier": "10.1.8",
46
46
  "eslint-plugin-import": "2.32.0",
47
- "eslint-plugin-jest": "27.6.3",
47
+ "eslint-plugin-jest": "29.15.4",
48
48
  "eslint-plugin-prettier": "5.5.5",
49
49
  "eslint-plugin-prettier-doc": "^1.1.0",
50
50
  "jest": "30.3.0",
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) {
@@ -192,7 +192,12 @@ function needsParens(path, options) {
192
192
  case "bin":
193
193
  case "retif":
194
194
  if (key === "test" && !parent.trueExpr) {
195
- 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;
196
201
  }
197
202
 
198
203
  return true;
package/src/printer.mjs CHANGED
@@ -1199,31 +1199,16 @@ function printAttrs(path, options, print, { inline = false } = {}) {
1199
1199
  if (!path.node.attrGroups) {
1200
1200
  return [];
1201
1201
  }
1202
- path.each(() => {
1203
- const attrGroup = ["#["];
1202
+ path.each((attrGroupPath) => {
1204
1203
  if (!inline && allAttrs.length > 0) {
1205
1204
  allAttrs.push(hardline);
1206
1205
  }
1207
- attrGroup.push(softline);
1208
- path.each(() => {
1209
- const attrNode = path.node;
1210
- if (attrGroup.length > 2) {
1211
- attrGroup.push(",", line);
1212
- }
1213
- const attrStmt = [attrNode.name];
1214
- if (attrNode.args.length > 0) {
1215
- attrStmt.push(printArgumentsList(path, options, print, "args"));
1216
- }
1217
- attrGroup.push(group(attrStmt));
1218
- }, "attrs");
1219
1206
  allAttrs.push(
1220
- group([
1221
- indent(attrGroup),
1222
- ifBreak(shouldPrintComma(options, 8.0) ? "," : ""),
1223
- softline,
1224
- "]",
1225
- inline ? ifBreak(softline, " ") : "",
1226
- ])
1207
+ printAllComments(
1208
+ attrGroupPath,
1209
+ () => printAttrGroup(attrGroupPath, options, print, { inline }),
1210
+ options
1211
+ )
1227
1212
  );
1228
1213
  }, "attrGroups");
1229
1214
  if (allAttrs.length === 0) {
@@ -1232,6 +1217,40 @@ function printAttrs(path, options, print, { inline = false } = {}) {
1232
1217
  return [...allAttrs, inline ? "" : hardline];
1233
1218
  }
1234
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
+
1235
1254
  function printClass(path, options, print) {
1236
1255
  const { node } = path;
1237
1256
  const isAnonymousClass = node.kind === "class" && node.isAnonymous;
@@ -1458,6 +1477,38 @@ function printFunction(path, options, print) {
1458
1477
  ];
1459
1478
  }
1460
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
+
1461
1512
  function printBodyControlStructure(
1462
1513
  path,
1463
1514
  options,
@@ -1766,14 +1817,13 @@ function printNode(path, options, print) {
1766
1817
  node.parenthesizedExpression ? ")" : "",
1767
1818
  ];
1768
1819
  case "parameter": {
1769
- let promoted = "";
1770
- if (node.flags === 1) {
1771
- promoted = "public ";
1772
- } else if (node.flags === 2) {
1773
- promoted = "protected ";
1774
- } else if (node.flags === 4) {
1775
- promoted = "private ";
1776
- }
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
+ ];
1777
1827
  const name = [
1778
1828
  ...printAttrs(path, options, print, { inline: true }),
1779
1829
  promoted,
@@ -1785,6 +1835,7 @@ function printNode(path, options, print) {
1785
1835
  "$",
1786
1836
  print("name"),
1787
1837
  ];
1838
+ const hooks = printPropertyHooks(path, options, print);
1788
1839
 
1789
1840
  if (node.value) {
1790
1841
  return group([
@@ -1802,10 +1853,11 @@ function printNode(path, options, print) {
1802
1853
  false,
1803
1854
  options
1804
1855
  ),
1856
+ hooks,
1805
1857
  ]);
1806
1858
  }
1807
1859
 
1808
- return name;
1860
+ return [name, hooks];
1809
1861
  }
1810
1862
  case "variadic":
1811
1863
  return ["...", print("what")];
@@ -1827,7 +1879,41 @@ function printNode(path, options, print) {
1827
1879
  ),
1828
1880
  ]
1829
1881
  : "",
1882
+ printPropertyHooks(path, options, print),
1830
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
+ }
1831
1917
  case "propertystatement": {
1832
1918
  const attrs = [];
1833
1919
  path.each(() => {
@@ -1850,10 +1936,18 @@ function printNode(path, options, print) {
1850
1936
 
1851
1937
  return group([
1852
1938
  ...attrs,
1939
+ node.isAbstract ? "abstract " : "",
1940
+ node.isFinal ? "final " : "",
1853
1941
  hasVisibility
1854
1942
  ? [node.visibility === null ? "var" : node.visibility, ""]
1855
1943
  : "",
1856
- 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
+ : "",
1857
1951
  firstProperty ? [" ", firstProperty] : "",
1858
1952
  indent(
1859
1953
  printed.slice(1).map((p) => [",", hasValue ? hardline : line, p])
@@ -2142,11 +2236,64 @@ function printNode(path, options, print) {
2142
2236
 
2143
2237
  return parts;
2144
2238
  }
2145
- 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
+
2146
2277
  return [
2147
- "clone ",
2148
- 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
+ ),
2149
2295
  ];
2296
+ }
2150
2297
  case "propertylookup":
2151
2298
  case "nullsafepropertylookup":
2152
2299
  case "staticlookup":
@@ -2306,6 +2453,10 @@ function printNode(path, options, print) {
2306
2453
  parent.curly) ||
2307
2454
  (parentParent &&
2308
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 &&
2309
2460
  parentParent.kind === "encapsedpart" &&
2310
2461
  parentParent.syntax === "simple" &&
2311
2462
  parentParent.curly)
@@ -2909,6 +3060,7 @@ function printNode(path, options, print) {
2909
3060
 
2910
3061
  case "enumcase":
2911
3062
  return group([
3063
+ ...printAttrs(path, options, print),
2912
3064
  "case ",
2913
3065
  print("name"),
2914
3066
  node.value
package/src/util.mjs CHANGED
@@ -354,6 +354,13 @@ function lineShouldEndWithSemicolon(path) {
354
354
  if (node.kind === "method" && node.isAbstract) {
355
355
  return true;
356
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
+ }
357
364
  if (node.kind === "method") {
358
365
  const { parent } = path;
359
366
  if (parent && parent.kind === "interface") {