@tsrx/prettier-plugin 0.3.105 → 0.3.107
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 +2 -2
- package/src/index.js +286 -153
- package/src/index.test.js +347 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tsrx/prettier-plugin",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.107",
|
|
4
4
|
"description": "Ripple plugin for Prettier",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "src/index.js",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"prettier": "^3.8.4"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@tsrx/core": "0.1.
|
|
30
|
+
"@tsrx/core": "0.1.46"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"src/"
|
package/src/index.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
/** @typedef {Partial<Pick<ParserOptions, 'singleQuote' | 'jsxSingleQuote' | 'semi' | 'trailingComma' | 'useTabs' | 'tabWidth' | 'singleAttributePerLine' | 'bracketSameLine' | 'bracketSpacing' | 'arrowParens' | 'originalText' | 'printWidth'>> & { locStart: (node: AST.NodeWithLocation) => number, locEnd: (node: AST.NodeWithLocation) => number }} RippleFormatOptions */
|
|
18
18
|
|
|
19
|
-
/** @typedef {{ isInAttribute?: boolean, isInArray?: boolean, allowInlineObject?: boolean, isConditionalTest?: boolean, isNestedConditional?: boolean, suppressLeadingComments?: boolean, suppressExpressionLeadingComments?: boolean, isInlineContext?: boolean, isStatement?: boolean, isLogicalAndOr?: boolean, allowShorthandProperty?: boolean, isFirstChild?: boolean, noBreakInside?: boolean, expandLastArg?: boolean, preferInlineSimpleUnionType?: boolean }} PrintArgs */
|
|
19
|
+
/** @typedef {{ isInAttribute?: boolean, isInArray?: boolean, allowInlineObject?: boolean, isConditionalTest?: boolean, isNestedConditional?: boolean, suppressLeadingComments?: boolean, suppressExpressionLeadingComments?: boolean, suppressOwnParens?: boolean, isInlineContext?: boolean, isStatement?: boolean, isLogicalAndOr?: boolean, allowShorthandProperty?: boolean, isFirstChild?: boolean, noBreakInside?: boolean, expandLastArg?: boolean, preferInlineSimpleUnionType?: boolean }} PrintArgs */
|
|
20
20
|
|
|
21
21
|
import { parseModule } from '@tsrx/core';
|
|
22
22
|
import { doc } from 'prettier';
|
|
@@ -423,6 +423,27 @@ function binaryExpressionNeedsParens(node, parent) {
|
|
|
423
423
|
return false;
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Check whether the operand of an `as`/`satisfies` cast must stay parenthesized.
|
|
428
|
+
* The cast binds at relational precedence, so lower-precedence operands parse
|
|
429
|
+
* differently without their parens: `a ?? b as string` is `a ?? (b as string)`,
|
|
430
|
+
* and mixing `??` with an unparenthesized cast operand is a TS syntax error.
|
|
431
|
+
* @param {AST.Node} expression - The cast operand
|
|
432
|
+
* @returns {boolean} - True if parentheses are required
|
|
433
|
+
*/
|
|
434
|
+
function castOperandNeedsParens(expression) {
|
|
435
|
+
switch (expression.type) {
|
|
436
|
+
case 'LogicalExpression':
|
|
437
|
+
case 'ArrowFunctionExpression':
|
|
438
|
+
case 'YieldExpression':
|
|
439
|
+
return true;
|
|
440
|
+
case 'BinaryExpression':
|
|
441
|
+
return getPrecedence(expression.operator) < PRECEDENCE['<'];
|
|
442
|
+
default:
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
426
447
|
/**
|
|
427
448
|
* Check if a parenthesized AssignmentExpression needs its parentheses preserved.
|
|
428
449
|
* @param {AST.AssignmentExpression} node - The expression node
|
|
@@ -1413,7 +1434,10 @@ function printRippleNode(node, path, options, print, args) {
|
|
|
1413
1434
|
}
|
|
1414
1435
|
|
|
1415
1436
|
const trailingDoc = shouldUseTrailingComma ? ifBreak(',', '') : '';
|
|
1416
|
-
|
|
1437
|
+
// All-or-nothing group instead of fill(): packing several elements
|
|
1438
|
+
// per wrapped line is never a fixpoint — the repacked output reparses
|
|
1439
|
+
// as a multiline array and would then break one element per line.
|
|
1440
|
+
nodeContent = group(['[', indent([softline, fillParts, trailingDoc]), softline, ']']);
|
|
1417
1441
|
break;
|
|
1418
1442
|
}
|
|
1419
1443
|
|
|
@@ -1578,7 +1602,7 @@ function printRippleNode(node, path, options, print, args) {
|
|
|
1578
1602
|
}
|
|
1579
1603
|
|
|
1580
1604
|
case 'MemberExpression':
|
|
1581
|
-
nodeContent = printMemberExpression(node, path, options, print);
|
|
1605
|
+
nodeContent = printMemberExpression(node, path, options, print, args);
|
|
1582
1606
|
break;
|
|
1583
1607
|
|
|
1584
1608
|
case 'MetaProperty':
|
|
@@ -1640,7 +1664,7 @@ function printRippleNode(node, path, options, print, args) {
|
|
|
1640
1664
|
|
|
1641
1665
|
// Preserve parentheses for type-annotated call expressions
|
|
1642
1666
|
// When parenthesized with leading comments, use grouping to allow breaking
|
|
1643
|
-
if (node.metadata?.parenthesized) {
|
|
1667
|
+
if (node.metadata?.parenthesized && !args?.suppressOwnParens) {
|
|
1644
1668
|
const hasLeadingComments = node.leadingComments && node.leadingComments.length > 0;
|
|
1645
1669
|
if (hasLeadingComments) {
|
|
1646
1670
|
// Group with softline to allow breaking after opening paren
|
|
@@ -1688,10 +1712,14 @@ function printRippleNode(node, path, options, print, args) {
|
|
|
1688
1712
|
(typePath) => print(typePath, { preferInlineSimpleUnionType: true }),
|
|
1689
1713
|
'typeAnnotation',
|
|
1690
1714
|
);
|
|
1715
|
+
const expressionDoc = path.call(print, 'expression');
|
|
1716
|
+
const operand = castOperandNeedsParens(node.expression)
|
|
1717
|
+
? ['(', expressionDoc, ')']
|
|
1718
|
+
: expressionDoc;
|
|
1691
1719
|
nodeContent =
|
|
1692
1720
|
node.typeAnnotation.type !== 'TSTypeLiteral' && willBreak(typeAnnotation)
|
|
1693
|
-
? [
|
|
1694
|
-
: [
|
|
1721
|
+
? [operand, ' as', indent([line, typeAnnotation])]
|
|
1722
|
+
: [operand, ' as ', typeAnnotation];
|
|
1695
1723
|
break;
|
|
1696
1724
|
}
|
|
1697
1725
|
|
|
@@ -1700,10 +1728,14 @@ function printRippleNode(node, path, options, print, args) {
|
|
|
1700
1728
|
(typePath) => print(typePath, { preferInlineSimpleUnionType: true }),
|
|
1701
1729
|
'typeAnnotation',
|
|
1702
1730
|
);
|
|
1731
|
+
const expressionDoc = path.call(print, 'expression');
|
|
1732
|
+
const operand = castOperandNeedsParens(node.expression)
|
|
1733
|
+
? ['(', expressionDoc, ')']
|
|
1734
|
+
: expressionDoc;
|
|
1703
1735
|
nodeContent =
|
|
1704
1736
|
node.typeAnnotation.type !== 'TSTypeLiteral' && willBreak(typeAnnotation)
|
|
1705
|
-
? [
|
|
1706
|
-
: [
|
|
1737
|
+
? [operand, ' satisfies', indent([line, typeAnnotation])]
|
|
1738
|
+
: [operand, ' satisfies ', typeAnnotation];
|
|
1707
1739
|
break;
|
|
1708
1740
|
}
|
|
1709
1741
|
|
|
@@ -1892,22 +1924,34 @@ function printRippleNode(node, path, options, print, args) {
|
|
|
1892
1924
|
}
|
|
1893
1925
|
case 'Identifier': {
|
|
1894
1926
|
// Simple case - just return the name directly like Prettier core
|
|
1927
|
+
const parent = path.getParentNode();
|
|
1928
|
+
// The definite-assignment assertion (`let x!: T`) lives on the declarator
|
|
1929
|
+
const definiteMarker =
|
|
1930
|
+
parent && parent.type === 'VariableDeclarator' && parent.id === node && parent.definite
|
|
1931
|
+
? '!'
|
|
1932
|
+
: '';
|
|
1895
1933
|
let identifierContent;
|
|
1896
1934
|
if (node.typeAnnotation) {
|
|
1897
1935
|
const optionalMarker = node.optional ? '?' : '';
|
|
1898
|
-
identifierContent = [
|
|
1936
|
+
identifierContent = [
|
|
1937
|
+
node.name,
|
|
1938
|
+
definiteMarker,
|
|
1939
|
+
optionalMarker,
|
|
1940
|
+
': ',
|
|
1941
|
+
path.call(print, 'typeAnnotation'),
|
|
1942
|
+
];
|
|
1899
1943
|
} else {
|
|
1900
|
-
identifierContent = node.name;
|
|
1944
|
+
identifierContent = definiteMarker ? [node.name, definiteMarker] : node.name;
|
|
1901
1945
|
}
|
|
1902
1946
|
// Preserve parentheses for type-cast identifiers, but only if:
|
|
1903
1947
|
// 1. The identifier itself is marked as parenthesized
|
|
1904
1948
|
// 2. The parent is NOT handling parentheses itself (MemberExpression, AssignmentExpression, etc.)
|
|
1905
|
-
const parent = path.getParentNode();
|
|
1906
1949
|
const parentHandlesParens =
|
|
1907
1950
|
parent &&
|
|
1908
1951
|
(parent.type === 'MemberExpression' ||
|
|
1909
1952
|
(parent.type === 'AssignmentExpression' && parent.left === node));
|
|
1910
|
-
const shouldAddParens =
|
|
1953
|
+
const shouldAddParens =
|
|
1954
|
+
node.metadata?.parenthesized && !parentHandlesParens && !args?.suppressOwnParens;
|
|
1911
1955
|
if (shouldAddParens) {
|
|
1912
1956
|
nodeContent = ['(', identifierContent, ')'];
|
|
1913
1957
|
} else {
|
|
@@ -2068,8 +2112,26 @@ function printRippleNode(node, path, options, print, args) {
|
|
|
2068
2112
|
/** @type {Doc[]} */
|
|
2069
2113
|
const parts = ['return'];
|
|
2070
2114
|
if (node.argument) {
|
|
2071
|
-
|
|
2072
|
-
|
|
2115
|
+
if (argumentHasOwnLineLeadingComment(node.argument)) {
|
|
2116
|
+
// The comment prints on its own line, which would separate `return`
|
|
2117
|
+
// from its argument and trigger ASI — keep the argument in parens.
|
|
2118
|
+
// These parens replace any the argument would print for itself.
|
|
2119
|
+
parts.push(
|
|
2120
|
+
' (',
|
|
2121
|
+
indent([
|
|
2122
|
+
hardline,
|
|
2123
|
+
path.call(
|
|
2124
|
+
(argumentPath) => print(argumentPath, { suppressOwnParens: true }),
|
|
2125
|
+
'argument',
|
|
2126
|
+
),
|
|
2127
|
+
]),
|
|
2128
|
+
hardline,
|
|
2129
|
+
')',
|
|
2130
|
+
);
|
|
2131
|
+
} else {
|
|
2132
|
+
parts.push(' ');
|
|
2133
|
+
parts.push(path.call(print, 'argument'));
|
|
2134
|
+
}
|
|
2073
2135
|
}
|
|
2074
2136
|
parts.push(semi(options));
|
|
2075
2137
|
nodeContent = parts;
|
|
@@ -2247,7 +2309,7 @@ function printRippleNode(node, path, options, print, args) {
|
|
|
2247
2309
|
}
|
|
2248
2310
|
|
|
2249
2311
|
// Wrap in parentheses if metadata indicates they were present
|
|
2250
|
-
if (node.metadata?.parenthesized) {
|
|
2312
|
+
if (node.metadata?.parenthesized && !args?.suppressOwnParens) {
|
|
2251
2313
|
result = ['(', result, ')'];
|
|
2252
2314
|
}
|
|
2253
2315
|
|
|
@@ -2271,7 +2333,7 @@ function printRippleNode(node, path, options, print, args) {
|
|
|
2271
2333
|
}
|
|
2272
2334
|
|
|
2273
2335
|
case 'MemberExpression':
|
|
2274
|
-
nodeContent = printMemberExpression(node, path, options, print);
|
|
2336
|
+
nodeContent = printMemberExpression(node, path, options, print, args);
|
|
2275
2337
|
break;
|
|
2276
2338
|
|
|
2277
2339
|
case 'ObjectPattern':
|
|
@@ -2737,12 +2799,8 @@ function printFunctionExpression(node, path, options, print) {
|
|
|
2737
2799
|
parts.push(' ');
|
|
2738
2800
|
}
|
|
2739
2801
|
|
|
2740
|
-
// Print parameters
|
|
2741
|
-
|
|
2742
|
-
parts.push(group(paramsPart)); // Handle return type annotation
|
|
2743
|
-
if (node.returnType) {
|
|
2744
|
-
parts.push(': ', path.call(print, 'returnType'));
|
|
2745
|
-
}
|
|
2802
|
+
// Print parameters and return type as a single group
|
|
2803
|
+
parts.push(printFunctionSignature(node, path, options, print));
|
|
2746
2804
|
|
|
2747
2805
|
parts.push(' ');
|
|
2748
2806
|
parts.push(path.call(print, 'body'));
|
|
@@ -2790,12 +2848,8 @@ function printArrowFunction(node, path, options, print, args) {
|
|
|
2790
2848
|
) {
|
|
2791
2849
|
parts.push(path.call(print, 'params', 0));
|
|
2792
2850
|
} else {
|
|
2793
|
-
// Print parameters
|
|
2794
|
-
|
|
2795
|
-
parts.push(group(paramsPart));
|
|
2796
|
-
} // Handle return type annotation
|
|
2797
|
-
if (node.returnType) {
|
|
2798
|
-
parts.push(': ', path.call(print, 'returnType'));
|
|
2851
|
+
// Print parameters and return type as a single group
|
|
2852
|
+
parts.push(printFunctionSignature(node, path, options, print));
|
|
2799
2853
|
}
|
|
2800
2854
|
|
|
2801
2855
|
// For block statements, print the body directly to get proper formatting
|
|
@@ -2808,7 +2862,6 @@ function printArrowFunction(node, path, options, print, args) {
|
|
|
2808
2862
|
// to avoid ambiguity with block statements or to clarify intent
|
|
2809
2863
|
const bodyDoc = path.call(print, 'body');
|
|
2810
2864
|
const groupId = Symbol('arrow');
|
|
2811
|
-
const shouldBreakBody = shouldBreakArrowExpressionBody(node.body, options, args);
|
|
2812
2865
|
/** @type {Doc | Doc[]} */
|
|
2813
2866
|
let bodyContent;
|
|
2814
2867
|
if (
|
|
@@ -2820,21 +2873,26 @@ function printArrowFunction(node, path, options, print, args) {
|
|
|
2820
2873
|
} else {
|
|
2821
2874
|
bodyContent = bodyDoc;
|
|
2822
2875
|
}
|
|
2823
|
-
if (
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
group([...parts, ' => ', bodyContent]),
|
|
2829
|
-
group([...parts, ' =>', indent([hardline, bodyContent])]),
|
|
2830
|
-
]);
|
|
2831
|
-
}
|
|
2832
|
-
parts.push(
|
|
2833
|
-
' =>',
|
|
2834
|
-
group(indent(line), { id: groupId }),
|
|
2835
|
-
indentIfBreak(bodyContent, { groupId }),
|
|
2836
|
-
);
|
|
2876
|
+
if (isTemplateExpression(node.body)) {
|
|
2877
|
+
return conditionalGroup([
|
|
2878
|
+
group([...parts, ' => ', bodyContent]),
|
|
2879
|
+
group([...parts, ' =>', indent([hardline, bodyContent])]),
|
|
2880
|
+
]);
|
|
2837
2881
|
}
|
|
2882
|
+
if (node.body.type === 'BinaryExpression' || node.body.type === 'LogicalExpression') {
|
|
2883
|
+
// Keep the body inline when it fits; otherwise break right after `=>`
|
|
2884
|
+
// so the body starts on its own line. An inner group scoped to the body
|
|
2885
|
+
// makes that call from the printed doc (not the original source span,
|
|
2886
|
+
// which kept the two passes disagreeing) and still works when the
|
|
2887
|
+
// parameter list itself breaks.
|
|
2888
|
+
parts.push(' =>', group(indent([line, bodyContent])));
|
|
2889
|
+
return group(parts);
|
|
2890
|
+
}
|
|
2891
|
+
parts.push(
|
|
2892
|
+
' =>',
|
|
2893
|
+
group(indent(line), { id: groupId }),
|
|
2894
|
+
indentIfBreak(bodyContent, { groupId }),
|
|
2895
|
+
);
|
|
2838
2896
|
}
|
|
2839
2897
|
|
|
2840
2898
|
return group(parts);
|
|
@@ -2849,25 +2907,6 @@ function isTemplateExpression(node) {
|
|
|
2849
2907
|
return node.type === 'JSXElement' || node.type === 'JSXFragment';
|
|
2850
2908
|
}
|
|
2851
2909
|
|
|
2852
|
-
/**
|
|
2853
|
-
* Check whether a braced attribute expression should close on its own line.
|
|
2854
|
-
* @param {AST.Node} node - The expression inside the attribute braces
|
|
2855
|
-
* @param {RippleFormatOptions} options
|
|
2856
|
-
* @param {AST.Node} [attributeNode]
|
|
2857
|
-
* @returns {boolean}
|
|
2858
|
-
*/
|
|
2859
|
-
function shouldBreakAttributeExpressionClosingBrace(node, options, attributeNode = node) {
|
|
2860
|
-
return (
|
|
2861
|
-
node.type === 'ArrowFunctionExpression' &&
|
|
2862
|
-
node.body &&
|
|
2863
|
-
isTemplateExpression(node.body) &&
|
|
2864
|
-
sourceSpanExceedsPrintWidth(
|
|
2865
|
-
/** @type {AST.NodeWithLocation} */ (/** @type {unknown} */ (attributeNode ?? node)),
|
|
2866
|
-
options,
|
|
2867
|
-
)
|
|
2868
|
-
);
|
|
2869
|
-
}
|
|
2870
|
-
|
|
2871
2910
|
/**
|
|
2872
2911
|
* Print an export default declaration
|
|
2873
2912
|
* @param {AST.ExportDefaultDeclaration} node - The export default node
|
|
@@ -2904,10 +2943,50 @@ function shouldHugTheOnlyFunctionParameter(node) {
|
|
|
2904
2943
|
parameter.type === 'ArrayPattern' ||
|
|
2905
2944
|
(parameter.type === 'Identifier' &&
|
|
2906
2945
|
!!parameter.typeAnnotation &&
|
|
2907
|
-
parameter.typeAnnotation.type === 'TSTypeAnnotation'
|
|
2946
|
+
parameter.typeAnnotation.type === 'TSTypeAnnotation' &&
|
|
2947
|
+
isHuggableParameterType(parameter.typeAnnotation.typeAnnotation)))
|
|
2908
2948
|
);
|
|
2909
2949
|
}
|
|
2910
2950
|
|
|
2951
|
+
/**
|
|
2952
|
+
* Check if a type node is an object-like type (object literal or mapped type)
|
|
2953
|
+
* @param {AST.Node | undefined} node - The type node
|
|
2954
|
+
* @returns {boolean}
|
|
2955
|
+
*/
|
|
2956
|
+
function isObjectType(node) {
|
|
2957
|
+
return !!node && (node.type === 'TSTypeLiteral' || node.type === 'TSMappedType');
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
/**
|
|
2961
|
+
* Check if a parameter's type annotation should keep the parameter hugged.
|
|
2962
|
+
* Object-like types hug like vanilla prettier; additionally a type reference
|
|
2963
|
+
* wrapping a single object type (`props: Props<{ ... }>`) hugs, since that is
|
|
2964
|
+
* the common TSRX component-props shape. Other references, like a plain
|
|
2965
|
+
* `initialState: State`, leave the parameter list free to break.
|
|
2966
|
+
* @param {AST.Node | undefined} node - The type node
|
|
2967
|
+
* @returns {boolean}
|
|
2968
|
+
*/
|
|
2969
|
+
function isHuggableParameterType(node) {
|
|
2970
|
+
if (isObjectType(node)) {
|
|
2971
|
+
return true;
|
|
2972
|
+
}
|
|
2973
|
+
if (node?.type === 'TSIntersectionType') {
|
|
2974
|
+
const types = /** @type {AST.TSIntersectionType} */ (node).types;
|
|
2975
|
+
return types?.length > 0 && isHuggableParameterType(types[types.length - 1]);
|
|
2976
|
+
}
|
|
2977
|
+
if (node?.type === 'TSTypeReference') {
|
|
2978
|
+
const typeArguments =
|
|
2979
|
+
/** @type {AST.TSTypeReference & { typeParameters?: AST.TSTypeParameterInstantiation }} */ (
|
|
2980
|
+
node
|
|
2981
|
+
).typeArguments ??
|
|
2982
|
+
/** @type {AST.TSTypeReference & { typeParameters?: AST.TSTypeParameterInstantiation }} */ (
|
|
2983
|
+
node
|
|
2984
|
+
).typeParameters;
|
|
2985
|
+
return typeArguments?.params?.length === 1 && isObjectType(typeArguments.params[0]);
|
|
2986
|
+
}
|
|
2987
|
+
return false;
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2911
2990
|
/**
|
|
2912
2991
|
* Print function parameters with proper formatting
|
|
2913
2992
|
* @param {AstPath<AST.FunctionExpression | AST.ArrowFunctionExpression | AST.TSDeclareFunction | AST.FunctionDeclaration>} path - The function path
|
|
@@ -2963,6 +3042,56 @@ function printFunctionParameters(path, options, print) {
|
|
|
2963
3042
|
];
|
|
2964
3043
|
}
|
|
2965
3044
|
|
|
3045
|
+
/**
|
|
3046
|
+
* Check whether the parameter list should be grouped separately from the return
|
|
3047
|
+
* type, so a breaking return type does not force the parameters to break too.
|
|
3048
|
+
* @param {AST.FunctionExpression | AST.ArrowFunctionExpression | AST.TSDeclareFunction | AST.FunctionDeclaration} functionNode - The function node
|
|
3049
|
+
* @param {Doc} returnTypeDoc - The printed return type
|
|
3050
|
+
* @returns {boolean}
|
|
3051
|
+
*/
|
|
3052
|
+
function shouldGroupFunctionParameters(functionNode, returnTypeDoc) {
|
|
3053
|
+
const returnTypeNode = functionNode.returnType?.typeAnnotation;
|
|
3054
|
+
const typeParameters = functionNode.typeParameters?.params;
|
|
3055
|
+
if (typeParameters) {
|
|
3056
|
+
if (typeParameters.length > 1) {
|
|
3057
|
+
return false;
|
|
3058
|
+
}
|
|
3059
|
+
if (typeParameters.length === 1) {
|
|
3060
|
+
const typeParameter = typeParameters[0];
|
|
3061
|
+
if (typeParameter.constraint || typeParameter.default) {
|
|
3062
|
+
return false;
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
return (
|
|
3067
|
+
getFunctionParameters(functionNode).length === 1 &&
|
|
3068
|
+
(isObjectType(returnTypeNode) || willBreak(returnTypeDoc))
|
|
3069
|
+
);
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
/**
|
|
3073
|
+
* Print function parameters together with the return type as a single group, so
|
|
3074
|
+
* the fitter breaks the parameter list before type arguments nested in the
|
|
3075
|
+
* return type, matching vanilla prettier's signature layout.
|
|
3076
|
+
* @param {AST.FunctionExpression | AST.ArrowFunctionExpression | AST.TSDeclareFunction | AST.FunctionDeclaration} node - The function node
|
|
3077
|
+
* @param {AstPath<AST.FunctionExpression | AST.ArrowFunctionExpression | AST.TSDeclareFunction | AST.FunctionDeclaration>} path - The function path
|
|
3078
|
+
* @param {RippleFormatOptions} options - Prettier options
|
|
3079
|
+
* @param {PrintFn} print - Print callback
|
|
3080
|
+
* @returns {Doc}
|
|
3081
|
+
*/
|
|
3082
|
+
function printFunctionSignature(node, path, options, print) {
|
|
3083
|
+
const paramsPart = printFunctionParameters(path, options, print);
|
|
3084
|
+
if (!node.returnType) {
|
|
3085
|
+
return group(paramsPart);
|
|
3086
|
+
}
|
|
3087
|
+
/** @type {Doc[]} */
|
|
3088
|
+
const returnTypeDoc = [': ', path.call(print, 'returnType')];
|
|
3089
|
+
if (shouldGroupFunctionParameters(node, returnTypeDoc)) {
|
|
3090
|
+
return group([group(paramsPart), ...returnTypeDoc]);
|
|
3091
|
+
}
|
|
3092
|
+
return group([...paramsPart, ...returnTypeDoc]);
|
|
3093
|
+
}
|
|
3094
|
+
|
|
2966
3095
|
/**
|
|
2967
3096
|
* Check if a node is spread-like (SpreadElement or RestElement)
|
|
2968
3097
|
* @param {AST.Node} node - The AST node
|
|
@@ -3060,34 +3189,6 @@ function shouldHugArrowFunctions(args) {
|
|
|
3060
3189
|
return firstBlockIndex === 0;
|
|
3061
3190
|
}
|
|
3062
3191
|
|
|
3063
|
-
/**
|
|
3064
|
-
* Check whether a node's original source span exceeds the configured print width.
|
|
3065
|
-
* @param {AST.NodeWithLocation} node - The node to check
|
|
3066
|
-
* @param {RippleFormatOptions} options - Prettier options
|
|
3067
|
-
* @returns {boolean}
|
|
3068
|
-
*/
|
|
3069
|
-
function sourceSpanExceedsPrintWidth(node, options) {
|
|
3070
|
-
const printWidth = options.printWidth ?? 80;
|
|
3071
|
-
if (!options.originalText || node.start === undefined || node.end === undefined) {
|
|
3072
|
-
return false;
|
|
3073
|
-
}
|
|
3074
|
-
return options.originalText.slice(node.start, node.end).length > printWidth;
|
|
3075
|
-
}
|
|
3076
|
-
|
|
3077
|
-
/**
|
|
3078
|
-
* Check if an arrow expression body should break immediately after `=>`.
|
|
3079
|
-
* @param {AST.Expression} node - The arrow body expression
|
|
3080
|
-
* @param {RippleFormatOptions} options - Prettier options
|
|
3081
|
-
* @param {PrintArgs} [args] - Additional context arguments
|
|
3082
|
-
* @returns {boolean}
|
|
3083
|
-
*/
|
|
3084
|
-
function shouldBreakArrowExpressionBody(node, options, args) {
|
|
3085
|
-
return (
|
|
3086
|
-
(node.type === 'BinaryExpression' || node.type === 'LogicalExpression') &&
|
|
3087
|
-
sourceSpanExceedsPrintWidth(/** @type {AST.NodeWithLocation} */ (node), options)
|
|
3088
|
-
);
|
|
3089
|
-
}
|
|
3090
|
-
|
|
3091
3192
|
/**
|
|
3092
3193
|
* Print call expression arguments
|
|
3093
3194
|
* @param {AstPath<AST.CallExpression>} path - The call path
|
|
@@ -3337,14 +3438,8 @@ function printTSDeclareFunction(node, path, options, print) {
|
|
|
3337
3438
|
}
|
|
3338
3439
|
}
|
|
3339
3440
|
|
|
3340
|
-
// Print parameters
|
|
3341
|
-
|
|
3342
|
-
parts.push(group(paramsPart));
|
|
3343
|
-
|
|
3344
|
-
// Handle return type annotation
|
|
3345
|
-
if (node.returnType) {
|
|
3346
|
-
parts.push(': ', path.call(print, 'returnType'));
|
|
3347
|
-
}
|
|
3441
|
+
// Print parameters and return type as a single group
|
|
3442
|
+
parts.push(printFunctionSignature(node, path, options, print));
|
|
3348
3443
|
|
|
3349
3444
|
// TSDeclareFunction ends with semicolon, no body
|
|
3350
3445
|
parts.push(';');
|
|
@@ -3389,14 +3484,8 @@ function printFunctionDeclaration(node, path, options, print) {
|
|
|
3389
3484
|
}
|
|
3390
3485
|
}
|
|
3391
3486
|
|
|
3392
|
-
// Print parameters
|
|
3393
|
-
|
|
3394
|
-
parts.push(group(paramsPart));
|
|
3395
|
-
|
|
3396
|
-
// Handle return type annotation
|
|
3397
|
-
if (node.returnType) {
|
|
3398
|
-
parts.push(': ', path.call(print, 'returnType'));
|
|
3399
|
-
}
|
|
3487
|
+
// Print parameters and return type as a single group
|
|
3488
|
+
parts.push(printFunctionSignature(node, path, options, print));
|
|
3400
3489
|
|
|
3401
3490
|
parts.push(' ');
|
|
3402
3491
|
parts.push(path.call(print, 'body'));
|
|
@@ -4119,9 +4208,10 @@ function printMethodDefinition(node, path, options, print) {
|
|
|
4119
4208
|
* @param {AstPath<AST.MemberExpression>} path - The AST path
|
|
4120
4209
|
* @param {RippleFormatOptions} options - Prettier options
|
|
4121
4210
|
* @param {PrintFn} print - Print callback
|
|
4211
|
+
* @param {PrintArgs} [args] - Additional context arguments
|
|
4122
4212
|
* @returns {Doc}
|
|
4123
4213
|
*/
|
|
4124
|
-
function printMemberExpression(node, path, options, print) {
|
|
4214
|
+
function printMemberExpression(node, path, options, print, args) {
|
|
4125
4215
|
let objectPart = path.call(print, 'object');
|
|
4126
4216
|
// Preserve parentheses around the object when present
|
|
4127
4217
|
if (node.object.metadata?.parenthesized) {
|
|
@@ -4139,7 +4229,7 @@ function printMemberExpression(node, path, options, print) {
|
|
|
4139
4229
|
}
|
|
4140
4230
|
|
|
4141
4231
|
// Preserve parentheses around the entire member expression when present
|
|
4142
|
-
if (node.metadata?.parenthesized) {
|
|
4232
|
+
if (node.metadata?.parenthesized && !args?.suppressOwnParens) {
|
|
4143
4233
|
// Check if there are leading comments - if so, use group with softlines to allow breaking
|
|
4144
4234
|
const hasLeadingComments = node.leadingComments && node.leadingComments.length > 0;
|
|
4145
4235
|
if (hasLeadingComments) {
|
|
@@ -4323,12 +4413,50 @@ function printTaggedTemplateExpression(node, path, options, print) {
|
|
|
4323
4413
|
function printThrowStatement(node, path, options, print) {
|
|
4324
4414
|
/** @type {Doc[]} */
|
|
4325
4415
|
const parts = [];
|
|
4326
|
-
|
|
4327
|
-
|
|
4416
|
+
if (argumentHasOwnLineLeadingComment(node.argument)) {
|
|
4417
|
+
// Same ASI hazard as `return`: keep the argument attached via parens.
|
|
4418
|
+
// These parens replace any the argument would print for itself.
|
|
4419
|
+
parts.push(
|
|
4420
|
+
'throw (',
|
|
4421
|
+
indent([
|
|
4422
|
+
hardline,
|
|
4423
|
+
path.call((argumentPath) => print(argumentPath, { suppressOwnParens: true }), 'argument'),
|
|
4424
|
+
]),
|
|
4425
|
+
hardline,
|
|
4426
|
+
')',
|
|
4427
|
+
);
|
|
4428
|
+
} else {
|
|
4429
|
+
parts.push('throw ');
|
|
4430
|
+
parts.push(path.call(print, 'argument'));
|
|
4431
|
+
}
|
|
4328
4432
|
parts.push(semi(options));
|
|
4329
4433
|
return parts;
|
|
4330
4434
|
}
|
|
4331
4435
|
|
|
4436
|
+
/**
|
|
4437
|
+
* Check whether a return/throw argument has a leading comment that prints on
|
|
4438
|
+
* its own line (line comments always do; block comments only when they did not
|
|
4439
|
+
* share a line with the argument in the source).
|
|
4440
|
+
* @param {AST.Node | null | undefined} argument - The statement argument
|
|
4441
|
+
* @returns {boolean}
|
|
4442
|
+
*/
|
|
4443
|
+
function argumentHasOwnLineLeadingComment(argument) {
|
|
4444
|
+
if (!argument) {
|
|
4445
|
+
return false;
|
|
4446
|
+
}
|
|
4447
|
+
const comments = /** @type {AST.NodeWithMaybeComments} */ (argument).leadingComments;
|
|
4448
|
+
if (!comments) {
|
|
4449
|
+
return false;
|
|
4450
|
+
}
|
|
4451
|
+
return comments.some(
|
|
4452
|
+
(comment) =>
|
|
4453
|
+
comment.type === 'Line' ||
|
|
4454
|
+
!comment.loc ||
|
|
4455
|
+
!argument.loc ||
|
|
4456
|
+
comment.loc.end.line !== argument.loc.start.line,
|
|
4457
|
+
);
|
|
4458
|
+
}
|
|
4459
|
+
|
|
4332
4460
|
/**
|
|
4333
4461
|
* Print a TypeScript interface declaration
|
|
4334
4462
|
* @param {AST.TSInterfaceDeclaration} node - The interface declaration node
|
|
@@ -4524,19 +4652,28 @@ function printTSTypeParameterDeclaration(node, path, options, print) {
|
|
|
4524
4652
|
if (!node.params || node.params.length === 0) {
|
|
4525
4653
|
return '';
|
|
4526
4654
|
}
|
|
4527
|
-
/** @type {Doc[]} */
|
|
4528
|
-
const parts = [];
|
|
4529
|
-
parts.push('<');
|
|
4530
4655
|
const paramList = path.map(print, 'params');
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4656
|
+
|
|
4657
|
+
// In JSX-shaped files a lone `<T>` on an arrow function is ambiguous with a JSX
|
|
4658
|
+
// element, so a source-level trailing comma (`<T,>`) is syntactically meaningful
|
|
4659
|
+
// there. Keep single-param arrow generics flat and preserve that comma; breaking
|
|
4660
|
+
// them would add a trailing comma that flattens back on the next pass.
|
|
4661
|
+
const parent = /** @type {AST.Node | null} */ (path.getParentNode());
|
|
4662
|
+
if (parent?.type === 'ArrowFunctionExpression' && node.params.length === 1) {
|
|
4663
|
+
const trailing = node.extra?.trailingComma !== undefined ? ',' : '';
|
|
4664
|
+
return ['<', paramList[0], trailing, '>'];
|
|
4537
4665
|
}
|
|
4538
|
-
|
|
4539
|
-
return
|
|
4666
|
+
|
|
4667
|
+
return group([
|
|
4668
|
+
'<',
|
|
4669
|
+
indent([
|
|
4670
|
+
softline,
|
|
4671
|
+
join([',', line], paramList),
|
|
4672
|
+
ifBreak(shouldPrintComma(options, 'all') ? ',' : ''),
|
|
4673
|
+
]),
|
|
4674
|
+
softline,
|
|
4675
|
+
'>',
|
|
4676
|
+
]);
|
|
4540
4677
|
}
|
|
4541
4678
|
|
|
4542
4679
|
/**
|
|
@@ -4580,18 +4717,18 @@ function printTSTypeParameterInstantiation(node, path, options, print) {
|
|
|
4580
4717
|
|
|
4581
4718
|
const paramList = path.map(print, 'params');
|
|
4582
4719
|
|
|
4720
|
+
// Hug a lone object-type argument against the brackets: Foo<{ ... }>
|
|
4721
|
+
if (
|
|
4722
|
+
node.params.length === 1 &&
|
|
4723
|
+
isObjectType(node.params[0]) &&
|
|
4724
|
+
!hasComment(/** @type {AST.Node & AST.NodeWithMaybeComments} */ (node.params[0]))
|
|
4725
|
+
) {
|
|
4726
|
+
return ['<', paramList[0], '>'];
|
|
4727
|
+
}
|
|
4728
|
+
|
|
4583
4729
|
// Check if any param has line breaks (e.g., contains object types)
|
|
4584
4730
|
const hasBreakingParam = paramList.some((param) => willBreak(param));
|
|
4585
4731
|
|
|
4586
|
-
// Build inline version: <T, U>
|
|
4587
|
-
/** @type {Doc[]} */
|
|
4588
|
-
const inlineParts = ['<'];
|
|
4589
|
-
for (let i = 0; i < paramList.length; i++) {
|
|
4590
|
-
if (i > 0) inlineParts.push(', ');
|
|
4591
|
-
inlineParts.push(paramList[i]);
|
|
4592
|
-
}
|
|
4593
|
-
inlineParts.push('>');
|
|
4594
|
-
|
|
4595
4732
|
// If any param breaks, use the breaking version with proper indentation
|
|
4596
4733
|
if (hasBreakingParam) {
|
|
4597
4734
|
// Build breaking version: <\n T,\n U\n>
|
|
@@ -5349,9 +5486,16 @@ function printTSTypeLiteral(node, path, options, print) {
|
|
|
5349
5486
|
'}',
|
|
5350
5487
|
]);
|
|
5351
5488
|
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5489
|
+
if (!wasOriginallySingleLine(node)) {
|
|
5490
|
+
// A hardline-first conditionalGroup always picks that state (fits()
|
|
5491
|
+
// short-circuits to true on hardlines) while hiding the hardlines from
|
|
5492
|
+
// enclosing groups' break propagation — ancestors then stay flat and
|
|
5493
|
+
// print following siblings past printWidth. The multiline doc alone is
|
|
5494
|
+
// equivalent and propagates its breaks correctly.
|
|
5495
|
+
return multilineDoc;
|
|
5496
|
+
}
|
|
5497
|
+
|
|
5498
|
+
return conditionalGroup([inlineDoc, multilineDoc]);
|
|
5355
5499
|
}
|
|
5356
5500
|
|
|
5357
5501
|
/**
|
|
@@ -5520,26 +5664,16 @@ function printTSTypeReference(node, path, options, print) {
|
|
|
5520
5664
|
/** @type {Doc[]} */
|
|
5521
5665
|
const parts = [path.call(print, 'typeName')];
|
|
5522
5666
|
|
|
5523
|
-
// Handle both typeArguments and typeParameters (different AST variations)
|
|
5667
|
+
// Handle both typeArguments and typeParameters (different AST variations).
|
|
5668
|
+
// Both are TSTypeParameterInstantiation nodes, whose printer can break the
|
|
5669
|
+
// argument list when it does not fit.
|
|
5524
5670
|
if (node.typeArguments) {
|
|
5525
|
-
parts.push('
|
|
5526
|
-
const typeArgs = path.map(print, 'typeArguments', 'params');
|
|
5527
|
-
for (let i = 0; i < typeArgs.length; i++) {
|
|
5528
|
-
if (i > 0) parts.push(', ');
|
|
5529
|
-
parts.push(typeArgs[i]);
|
|
5530
|
-
}
|
|
5531
|
-
parts.push('>');
|
|
5671
|
+
parts.push(path.call(print, 'typeArguments'));
|
|
5532
5672
|
// @ts-expect-error - acorn-typescript uses typeParameters instead of typeArguments
|
|
5533
5673
|
// we normalize it in the analyze phase, but here we get the parser ast
|
|
5534
5674
|
} else if (node.typeParameters) {
|
|
5535
|
-
parts.push('<');
|
|
5536
5675
|
// @ts-expect-error - acorn-typescript uses typeParameters instead of typeArguments
|
|
5537
|
-
|
|
5538
|
-
for (let i = 0; i < typeParams.length; i++) {
|
|
5539
|
-
if (i > 0) parts.push(', ');
|
|
5540
|
-
parts.push(typeParams[i]);
|
|
5541
|
-
}
|
|
5542
|
-
parts.push('>');
|
|
5676
|
+
parts.push(path.call(print, 'typeParameters'));
|
|
5543
5677
|
}
|
|
5544
5678
|
|
|
5545
5679
|
return parts;
|
|
@@ -5829,7 +5963,9 @@ function isSimpleJSXExpressionChild(child) {
|
|
|
5829
5963
|
expression?.type === 'MemberExpression' ||
|
|
5830
5964
|
expression?.type === 'CallExpression' ||
|
|
5831
5965
|
expression?.type === 'BinaryExpression' ||
|
|
5832
|
-
expression?.type === 'LogicalExpression'
|
|
5966
|
+
expression?.type === 'LogicalExpression' ||
|
|
5967
|
+
// Text holes like `{expr as string}` are as simple as their operand
|
|
5968
|
+
expression?.type === 'TSAsExpression'
|
|
5833
5969
|
);
|
|
5834
5970
|
}
|
|
5835
5971
|
|
|
@@ -6548,9 +6684,6 @@ function printJSXAttribute(attr, path, options, print) {
|
|
|
6548
6684
|
'value',
|
|
6549
6685
|
'expression',
|
|
6550
6686
|
);
|
|
6551
|
-
if (shouldBreakAttributeExpressionClosingBrace(expression, options, attr)) {
|
|
6552
|
-
return [name, '={', exprDoc, hardline, '}'];
|
|
6553
|
-
}
|
|
6554
6687
|
return [name, '={', exprDoc, '}'];
|
|
6555
6688
|
}
|
|
6556
6689
|
|
package/src/index.test.js
CHANGED
|
@@ -55,15 +55,23 @@ describe('prettier-plugin', () => {
|
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
+
* Format tsrx source. Every call also verifies the output is a fixpoint:
|
|
59
|
+
* formatting must be single-pass idempotent (a second pass may not change
|
|
60
|
+
* a single byte), so the whole suite doubles as an idempotence corpus.
|
|
58
61
|
* @param {string} code
|
|
59
62
|
* @param {import('prettier').Options} [options]
|
|
60
63
|
*/
|
|
61
64
|
const format = async (code, options = {}) => {
|
|
62
|
-
|
|
65
|
+
/** @type {import('prettier').Options} */
|
|
66
|
+
const resolvedOptions = {
|
|
63
67
|
parser: 'tsrx',
|
|
64
68
|
plugins: [join(__dirname, 'index.js')],
|
|
65
69
|
...options,
|
|
66
|
-
}
|
|
70
|
+
};
|
|
71
|
+
const once = await prettier.format(code, resolvedOptions);
|
|
72
|
+
const twice = await prettier.format(once, resolvedOptions);
|
|
73
|
+
expect(twice, 'formatting must be idempotent (second pass changed the output)').toBe(once);
|
|
74
|
+
return once;
|
|
67
75
|
};
|
|
68
76
|
|
|
69
77
|
it('formats functions that return native elements', async () => {
|
|
@@ -888,8 +896,7 @@ const items=[1,2,3];
|
|
|
888
896
|
<List
|
|
889
897
|
items={props.items}
|
|
890
898
|
renderItem={(item) =>
|
|
891
|
-
<><ItemView {item} onSelect={props.onSelect} /></>
|
|
892
|
-
}
|
|
899
|
+
<><ItemView {item} onSelect={props.onSelect} /></>}
|
|
893
900
|
/>
|
|
894
901
|
}`;
|
|
895
902
|
const result = await format(input, { singleQuote: true, printWidth: 60 });
|
|
@@ -1243,7 +1250,8 @@ const items=[1,2,3];
|
|
|
1243
1250
|
it('should format destructured dynamic import() in Promise.all', async () => {
|
|
1244
1251
|
const input = `const [{ EditorState }, { oneDark }] = await Promise.all([import('@codemirror/state'), import('@codemirror/theme-one-dark')]);`;
|
|
1245
1252
|
const expected = `const [{ EditorState }, { oneDark }] = await Promise.all([
|
|
1246
|
-
import("@codemirror/state"),
|
|
1253
|
+
import("@codemirror/state"),
|
|
1254
|
+
import("@codemirror/theme-one-dark"),
|
|
1247
1255
|
]);`;
|
|
1248
1256
|
const result = await format(input);
|
|
1249
1257
|
expect(result).toBeWithNewline(expected);
|
|
@@ -3377,7 +3385,8 @@ function test() {
|
|
|
3377
3385
|
|
|
3378
3386
|
const expected = `const arr = [
|
|
3379
3387
|
1, /* comment 1 */
|
|
3380
|
-
2,
|
|
3388
|
+
2,
|
|
3389
|
+
3,
|
|
3381
3390
|
// comment 2
|
|
3382
3391
|
];`;
|
|
3383
3392
|
|
|
@@ -3763,6 +3772,45 @@ const deleteButton = container.querySelector(
|
|
|
3763
3772
|
expect(result).toBeWithNewline(expected);
|
|
3764
3773
|
});
|
|
3765
3774
|
|
|
3775
|
+
it('should keep comments attached to their type arguments and stay idempotent', async () => {
|
|
3776
|
+
const input = `interface Props {
|
|
3777
|
+
form: AppFieldExtendedReactFormApi<
|
|
3778
|
+
unknown,
|
|
3779
|
+
| undefined
|
|
3780
|
+
| FormAsyncValidateOrFn<unknown>, // this types it as 'never' in the render prop. It should prevent any
|
|
3781
|
+
// untyped meta passed to the handleSubmit by accident.
|
|
3782
|
+
NoInfer<TSubmitMeta>
|
|
3783
|
+
>;
|
|
3784
|
+
}`;
|
|
3785
|
+
// Matches vanilla prettier's typescript parser output for the same input.
|
|
3786
|
+
const expected = `interface Props {
|
|
3787
|
+
form: AppFieldExtendedReactFormApi<
|
|
3788
|
+
unknown,
|
|
3789
|
+
undefined | FormAsyncValidateOrFn<unknown>, // this types it as 'never' in the render prop. It should prevent any
|
|
3790
|
+
// untyped meta passed to the handleSubmit by accident.
|
|
3791
|
+
NoInfer<TSubmitMeta>
|
|
3792
|
+
>;
|
|
3793
|
+
}`;
|
|
3794
|
+
const options = { useTabs: true, tabWidth: 2, singleQuote: true, printWidth: 100 };
|
|
3795
|
+
const result = await format(input, options);
|
|
3796
|
+
expect(result).toBeWithNewline(expected);
|
|
3797
|
+
expect(await format(result, options)).toBe(result);
|
|
3798
|
+
});
|
|
3799
|
+
|
|
3800
|
+
it('should hug a lone object type argument against the angle brackets', async () => {
|
|
3801
|
+
const input = `function Button(props: PropsWithExtras<{
|
|
3802
|
+
variant: string;
|
|
3803
|
+
label: string;
|
|
3804
|
+
onClick: EventListener;
|
|
3805
|
+
}>) @{
|
|
3806
|
+
<button class={props.variant} onClick={props.onClick}>{props.label}</button>
|
|
3807
|
+
}`;
|
|
3808
|
+
const options = { useTabs: true, tabWidth: 2, singleQuote: true, printWidth: 100 };
|
|
3809
|
+
const result = await format(input, options);
|
|
3810
|
+
expect(result).toBeWithNewline(input);
|
|
3811
|
+
expect(await format(result, options)).toBe(result);
|
|
3812
|
+
});
|
|
3813
|
+
|
|
3766
3814
|
it('should not overindent multiline object type aliases', async () => {
|
|
3767
3815
|
const input = `type ModuleShape = {
|
|
3768
3816
|
default: ComponentType<{ value: string }>;
|
|
@@ -6493,4 +6541,297 @@ function RowList({ rows, Row }) {
|
|
|
6493
6541
|
expect(result).toBeWithNewline(expected);
|
|
6494
6542
|
});
|
|
6495
6543
|
});
|
|
6544
|
+
|
|
6545
|
+
describe('parens around as-cast operands', () => {
|
|
6546
|
+
it('keeps parens around a nullish coalescing operand of an as-cast', async () => {
|
|
6547
|
+
const input = `function App() {
|
|
6548
|
+
return <span>{(activeAuthor ?? "All authors") as string}</span>;
|
|
6549
|
+
}`;
|
|
6550
|
+
|
|
6551
|
+
const result = await format(input);
|
|
6552
|
+
expect(result).toBeWithNewline(input);
|
|
6553
|
+
});
|
|
6554
|
+
|
|
6555
|
+
it('keeps parens around logical and equality operands of as-casts', async () => {
|
|
6556
|
+
const input = `function App() {
|
|
6557
|
+
const a = (x || y) as string;
|
|
6558
|
+
const b = (x == y) as boolean;
|
|
6559
|
+
const c = (x ?? y) satisfies string;
|
|
6560
|
+
return <div>{a}</div>;
|
|
6561
|
+
}`;
|
|
6562
|
+
|
|
6563
|
+
const result = await format(input);
|
|
6564
|
+
expect(result).toBeWithNewline(input);
|
|
6565
|
+
});
|
|
6566
|
+
|
|
6567
|
+
it('does not add parens around higher-precedence operands of as-casts', async () => {
|
|
6568
|
+
const input = `function App() {
|
|
6569
|
+
const a = x + y as string;
|
|
6570
|
+
const b = x < y as unknown;
|
|
6571
|
+
return <div>{a}</div>;
|
|
6572
|
+
}`;
|
|
6573
|
+
|
|
6574
|
+
const result = await format(input);
|
|
6575
|
+
expect(result).toBeWithNewline(input);
|
|
6576
|
+
});
|
|
6577
|
+
});
|
|
6578
|
+
|
|
6579
|
+
describe('definite assignment assertions', () => {
|
|
6580
|
+
it('keeps the definite assignment assertion on variable declarations', async () => {
|
|
6581
|
+
const input = `function App() {
|
|
6582
|
+
let cleanup!: () => void;
|
|
6583
|
+
var count!: number;
|
|
6584
|
+
return <div />;
|
|
6585
|
+
}`;
|
|
6586
|
+
|
|
6587
|
+
const result = await format(input);
|
|
6588
|
+
expect(result).toBeWithNewline(input);
|
|
6589
|
+
});
|
|
6590
|
+
});
|
|
6591
|
+
|
|
6592
|
+
describe('idempotence', () => {
|
|
6593
|
+
/**
|
|
6594
|
+
* @param {string} code
|
|
6595
|
+
* @param {import('prettier').Options} [options]
|
|
6596
|
+
*/
|
|
6597
|
+
const expectStable = async (code, options = {}) => {
|
|
6598
|
+
const once = await format(code, options);
|
|
6599
|
+
const twice = await format(once, options);
|
|
6600
|
+
expect(twice).toBe(once);
|
|
6601
|
+
return once;
|
|
6602
|
+
};
|
|
6603
|
+
|
|
6604
|
+
it('keeps a return argument with leading line comments after the return keyword', async () => {
|
|
6605
|
+
const input = `function isXOrYInValid(xOrY: string | number | undefined) {
|
|
6606
|
+
return (
|
|
6607
|
+
// number that is not NaN or Infinity
|
|
6608
|
+
(typeof xOrY === 'number' && Number.isFinite(xOrY)) ||
|
|
6609
|
+
// for percentage
|
|
6610
|
+
typeof xOrY === 'string'
|
|
6611
|
+
);
|
|
6612
|
+
}`;
|
|
6613
|
+
|
|
6614
|
+
const once = await expectStable(input, {
|
|
6615
|
+
useTabs: true,
|
|
6616
|
+
singleQuote: true,
|
|
6617
|
+
printWidth: 100,
|
|
6618
|
+
});
|
|
6619
|
+
// The argument must stay attached to the return; printing the comment
|
|
6620
|
+
// between `return` and the expression triggers ASI and returns undefined.
|
|
6621
|
+
expect(once).not.toMatch(/return[;\s]*\/\//);
|
|
6622
|
+
});
|
|
6623
|
+
|
|
6624
|
+
it('does not double-wrap self-parenthesizing return and throw arguments', async () => {
|
|
6625
|
+
const input = `function f() {
|
|
6626
|
+
return (
|
|
6627
|
+
// pick the fallback
|
|
6628
|
+
cond ? a : b
|
|
6629
|
+
);
|
|
6630
|
+
}
|
|
6631
|
+
|
|
6632
|
+
function g() {
|
|
6633
|
+
throw (
|
|
6634
|
+
// wrap the cause
|
|
6635
|
+
makeError(cause)
|
|
6636
|
+
);
|
|
6637
|
+
}`;
|
|
6638
|
+
|
|
6639
|
+
const result = await format(input);
|
|
6640
|
+
expect(result).toBeWithNewline(input);
|
|
6641
|
+
});
|
|
6642
|
+
|
|
6643
|
+
it('breaks long logical arrow bodies after multiline type-literal params', async () => {
|
|
6644
|
+
const input = `function f() {
|
|
6645
|
+
const mapping = result.mappings.find(
|
|
6646
|
+
(mapping: {
|
|
6647
|
+
sourceOffsets: number[];
|
|
6648
|
+
generatedOffsets: number[];
|
|
6649
|
+
}) =>
|
|
6650
|
+
mapping.sourceOffsets[0] === source_offset &&
|
|
6651
|
+
mapping.generatedOffsets[0] === generated_offset &&
|
|
6652
|
+
mapping.lengths[0] === identifier.length,
|
|
6653
|
+
);
|
|
6654
|
+
}`;
|
|
6655
|
+
|
|
6656
|
+
// The multiline param type used to hide its hardlines from enclosing
|
|
6657
|
+
// groups (fits() short-circuits on hardlines inside conditionalGroup
|
|
6658
|
+
// states), so the body printed flat past printWidth.
|
|
6659
|
+
const result = await format(input, {
|
|
6660
|
+
useTabs: true,
|
|
6661
|
+
singleQuote: true,
|
|
6662
|
+
printWidth: 100,
|
|
6663
|
+
});
|
|
6664
|
+
expect(result).toBeWithNewline(input);
|
|
6665
|
+
});
|
|
6666
|
+
|
|
6667
|
+
it('stabilizes long arrow bodies with logical expressions in one pass', async () => {
|
|
6668
|
+
const input = `function useStack() {
|
|
6669
|
+
return horizontal
|
|
6670
|
+
? {
|
|
6671
|
+
defined: (d: AreaStackDatum<XScale, YScale>) =>
|
|
6672
|
+
isValidNumber(yScale(getStackValue(d.data))) && isValidNumber(xScale(getSecondItem(d))),
|
|
6673
|
+
}
|
|
6674
|
+
: null;
|
|
6675
|
+
}`;
|
|
6676
|
+
|
|
6677
|
+
await expectStable(input, {
|
|
6678
|
+
useTabs: true,
|
|
6679
|
+
singleQuote: true,
|
|
6680
|
+
printWidth: 100,
|
|
6681
|
+
});
|
|
6682
|
+
});
|
|
6683
|
+
|
|
6684
|
+
it('stabilizes deeply nested JSX attribute arrows returning JSX in one pass', async () => {
|
|
6685
|
+
const input = `function Parent() {
|
|
6686
|
+
return <div>
|
|
6687
|
+
<section>
|
|
6688
|
+
<article>
|
|
6689
|
+
<fieldset>
|
|
6690
|
+
<group.Subscribe
|
|
6691
|
+
children={(state) => (
|
|
6692
|
+
<span data-testid="state-lastName">{state.values.lastName}</span>
|
|
6693
|
+
)}
|
|
6694
|
+
/>
|
|
6695
|
+
</fieldset>
|
|
6696
|
+
</article>
|
|
6697
|
+
</section>
|
|
6698
|
+
</div>;
|
|
6699
|
+
}`;
|
|
6700
|
+
|
|
6701
|
+
await expectStable(input, {
|
|
6702
|
+
useTabs: true,
|
|
6703
|
+
singleQuote: true,
|
|
6704
|
+
printWidth: 100,
|
|
6705
|
+
});
|
|
6706
|
+
});
|
|
6707
|
+
});
|
|
6708
|
+
|
|
6709
|
+
describe('type parameter declarations', () => {
|
|
6710
|
+
/**
|
|
6711
|
+
* Formats twice and asserts the output is stable (idempotent).
|
|
6712
|
+
* @param {string} code
|
|
6713
|
+
* @param {import('prettier').Options} [options]
|
|
6714
|
+
*/
|
|
6715
|
+
const formatStable = async (code, options = {}) => {
|
|
6716
|
+
const once = await format(code, options);
|
|
6717
|
+
const twice = await format(once, options);
|
|
6718
|
+
expect(twice).toBe(once);
|
|
6719
|
+
return once;
|
|
6720
|
+
};
|
|
6721
|
+
|
|
6722
|
+
it('breaks long interface type parameter lists one per line with a trailing comma', async () => {
|
|
6723
|
+
const input = `export interface WithFieldGroupProps<TFieldGroupData, TFieldComponents extends Record<string, HookComponentType<any>>, TFormComponents extends Record<string, HookComponentType<any>>, TSubmitMeta, TRenderProps extends object = Record<string, never>> extends BaseFormOptions<TFieldGroupData, TSubmitMeta> {
|
|
6724
|
+
props?: TRenderProps;
|
|
6725
|
+
}`;
|
|
6726
|
+
const expected = `export interface WithFieldGroupProps<
|
|
6727
|
+
TFieldGroupData,
|
|
6728
|
+
TFieldComponents extends Record<string, HookComponentType<any>>,
|
|
6729
|
+
TFormComponents extends Record<string, HookComponentType<any>>,
|
|
6730
|
+
TSubmitMeta,
|
|
6731
|
+
TRenderProps extends object = Record<string, never>,
|
|
6732
|
+
> extends BaseFormOptions<TFieldGroupData, TSubmitMeta> {
|
|
6733
|
+
props?: TRenderProps;
|
|
6734
|
+
}`;
|
|
6735
|
+
|
|
6736
|
+
const result = await formatStable(input, {
|
|
6737
|
+
printWidth: 100,
|
|
6738
|
+
useTabs: true,
|
|
6739
|
+
singleQuote: true,
|
|
6740
|
+
});
|
|
6741
|
+
expect(result).toBeWithNewline(expected);
|
|
6742
|
+
});
|
|
6743
|
+
|
|
6744
|
+
it('prefers breaking the function parameter list over the type parameter list', async () => {
|
|
6745
|
+
const input = `export default function useStateWithCallback<State>(initialState: State): [State, SetStateWithCallback<State>] {
|
|
6746
|
+
return null;
|
|
6747
|
+
}`;
|
|
6748
|
+
const expected = `export default function useStateWithCallback<State>(
|
|
6749
|
+
initialState: State,
|
|
6750
|
+
): [State, SetStateWithCallback<State>] {
|
|
6751
|
+
return null;
|
|
6752
|
+
}`;
|
|
6753
|
+
|
|
6754
|
+
const result = await formatStable(input, {
|
|
6755
|
+
printWidth: 100,
|
|
6756
|
+
useTabs: true,
|
|
6757
|
+
singleQuote: true,
|
|
6758
|
+
});
|
|
6759
|
+
expect(result).toBeWithNewline(expected);
|
|
6760
|
+
});
|
|
6761
|
+
|
|
6762
|
+
it('breaks function type parameter lists that overflow on their own', async () => {
|
|
6763
|
+
const input = `function useAppForm<TFormData, TOnMount extends undefined | FormValidateOrFn<TFormData>, TOnChange extends undefined | FormValidateOrFn<TFormData>, TSubmitMeta>(props: FormOptions<TFormData, TOnMount, TOnChange, TSubmitMeta>): void {
|
|
6764
|
+
return;
|
|
6765
|
+
}`;
|
|
6766
|
+
const expected = `function useAppForm<
|
|
6767
|
+
TFormData,
|
|
6768
|
+
TOnMount extends undefined | FormValidateOrFn<TFormData>,
|
|
6769
|
+
TOnChange extends undefined | FormValidateOrFn<TFormData>,
|
|
6770
|
+
TSubmitMeta,
|
|
6771
|
+
>(props: FormOptions<TFormData, TOnMount, TOnChange, TSubmitMeta>): void {
|
|
6772
|
+
return;
|
|
6773
|
+
}`;
|
|
6774
|
+
|
|
6775
|
+
const result = await formatStable(input, {
|
|
6776
|
+
printWidth: 100,
|
|
6777
|
+
useTabs: true,
|
|
6778
|
+
singleQuote: true,
|
|
6779
|
+
});
|
|
6780
|
+
expect(result).toBeWithNewline(expected);
|
|
6781
|
+
});
|
|
6782
|
+
|
|
6783
|
+
it('stays idempotent when a single type parameter has to break', async () => {
|
|
6784
|
+
const input = `interface Container<TExtremelyLongParameterName extends Record<string, unknown>> {
|
|
6785
|
+
value: TExtremelyLongParameterName;
|
|
6786
|
+
}`;
|
|
6787
|
+
const expected = `interface Container<
|
|
6788
|
+
TExtremelyLongParameterName extends Record<string, unknown>,
|
|
6789
|
+
> {
|
|
6790
|
+
value: TExtremelyLongParameterName;
|
|
6791
|
+
}`;
|
|
6792
|
+
|
|
6793
|
+
const result = await formatStable(input);
|
|
6794
|
+
expect(result).toBeWithNewline(expected);
|
|
6795
|
+
});
|
|
6796
|
+
|
|
6797
|
+
it('preserves the trailing comma of single-param arrow function generics', async () => {
|
|
6798
|
+
const expected = `const identity = <T,>(value: T): T => value;`;
|
|
6799
|
+
|
|
6800
|
+
const result = await formatStable(expected);
|
|
6801
|
+
expect(result).toBeWithNewline(expected);
|
|
6802
|
+
});
|
|
6803
|
+
|
|
6804
|
+
it('keeps single-param arrow function generics without a trailing comma as-is', async () => {
|
|
6805
|
+
const expected = `const identity = <T>(value: T): T => value;`;
|
|
6806
|
+
|
|
6807
|
+
const result = await formatStable(expected);
|
|
6808
|
+
expect(result).toBeWithNewline(expected);
|
|
6809
|
+
});
|
|
6810
|
+
|
|
6811
|
+
it('drops meaningless trailing commas from non-arrow type parameter lists', async () => {
|
|
6812
|
+
const input = `function pick<State,>(value: State): State {
|
|
6813
|
+
return value;
|
|
6814
|
+
}`;
|
|
6815
|
+
const expected = `function pick<State>(value: State): State {
|
|
6816
|
+
return value;
|
|
6817
|
+
}`;
|
|
6818
|
+
|
|
6819
|
+
const result = await formatStable(input);
|
|
6820
|
+
expect(result).toBeWithNewline(expected);
|
|
6821
|
+
});
|
|
6822
|
+
|
|
6823
|
+
it('omits the trailing comma in broken type parameter lists when trailingComma is none', async () => {
|
|
6824
|
+
const input = `interface Container<TExtremelyLongParameterName extends Record<string, unknown>> {
|
|
6825
|
+
value: TExtremelyLongParameterName;
|
|
6826
|
+
}`;
|
|
6827
|
+
const expected = `interface Container<
|
|
6828
|
+
TExtremelyLongParameterName extends Record<string, unknown>
|
|
6829
|
+
> {
|
|
6830
|
+
value: TExtremelyLongParameterName;
|
|
6831
|
+
}`;
|
|
6832
|
+
|
|
6833
|
+
const result = await formatStable(input, { trailingComma: 'none' });
|
|
6834
|
+
expect(result).toBeWithNewline(expected);
|
|
6835
|
+
});
|
|
6836
|
+
});
|
|
6496
6837
|
});
|