@blumintinc/eslint-plugin-blumint 1.20.174 → 1.20.176
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/lib/index.js +1 -1
- package/lib/rules/enforce-assert-safe-object-key.js +28 -40
- package/lib/rules/enforce-firestore-set-merge.js +135 -26
- package/lib/rules/enforce-singular-type-names.js +140 -0
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +47 -28
- package/package.json +1 -1
- package/release-manifest.json +44 -0
package/lib/index.js
CHANGED
|
@@ -1302,11 +1302,18 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
1302
1302
|
* position the author wrote it, and hands assertSafe what it produces — the
|
|
1303
1303
|
* rewrite adds a validation, it does not move a dereference.
|
|
1304
1304
|
*
|
|
1305
|
-
* A
|
|
1306
|
-
*
|
|
1307
|
-
*
|
|
1305
|
+
* A coercion the author wrote is part of the key, so it is wrapped rather
|
|
1306
|
+
* than replaced: `assertSafe(String(id))` and ``assertSafe(`${id}`)``, not
|
|
1307
|
+
* `assertSafe(id)`. assertSafe VALIDATES a key, it does not coerce one —
|
|
1308
|
+
* it throws on any argument whose `typeof` is neither `string` nor
|
|
1309
|
+
* `number` — so dropping the `String()` call or the template hands it the
|
|
1310
|
+
* raw operand and changes what the key evaluates to. A boolean operand
|
|
1311
|
+
* turns a working `` record[`${flag}`] `` lookup into a throw (#2144).
|
|
1312
|
+
* Wrapping the key exactly as written is also what makes the sole
|
|
1313
|
+
* substitution behave like every other template: `` `${a}${b}` `` has
|
|
1314
|
+
* always been wrapped whole.
|
|
1308
1315
|
*/
|
|
1309
|
-
const reportWrittenKey = (written
|
|
1316
|
+
const reportWrittenKey = (written) => reportUseAssertSafe(written, context.sourceCode.getText(written));
|
|
1310
1317
|
/**
|
|
1311
1318
|
* Returns true when the identifier was initialized directly from an
|
|
1312
1319
|
* assertSafe(...) call, e.g. `const safeKey = assertSafe(rawKey)`.
|
|
@@ -1830,9 +1837,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
1830
1837
|
if (key.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
1831
1838
|
key.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
1832
1839
|
key.callee.name === 'String') {
|
|
1833
|
-
|
|
1834
|
-
const argText = context.sourceCode.getText(arg);
|
|
1835
|
-
reportWrittenKey(written, key, argText);
|
|
1840
|
+
reportWrittenKey(written);
|
|
1836
1841
|
}
|
|
1837
1842
|
// Check for template literals like `${id}`
|
|
1838
1843
|
if (key.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
|
|
@@ -1840,9 +1845,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
1840
1845
|
key.quasis.length === 2 &&
|
|
1841
1846
|
key.quasis[0].value.raw === '' &&
|
|
1842
1847
|
key.quasis[1].value.raw === '') {
|
|
1843
|
-
|
|
1844
|
-
const exprText = context.sourceCode.getText(expr);
|
|
1845
|
-
reportWrittenKey(written, key, exprText);
|
|
1848
|
+
reportWrittenKey(written);
|
|
1846
1849
|
}
|
|
1847
1850
|
}
|
|
1848
1851
|
},
|
|
@@ -1855,9 +1858,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
1855
1858
|
if (left.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
1856
1859
|
left.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
1857
1860
|
left.callee.name === 'String') {
|
|
1858
|
-
|
|
1859
|
-
const argText = context.sourceCode.getText(arg);
|
|
1860
|
-
reportWrittenKey(written, left, argText);
|
|
1861
|
+
reportWrittenKey(written);
|
|
1861
1862
|
}
|
|
1862
1863
|
// Check for template literals like `${id}`
|
|
1863
1864
|
if (left.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
|
|
@@ -1865,9 +1866,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
1865
1866
|
left.quasis.length === 2 &&
|
|
1866
1867
|
left.quasis[0].value.raw === '' &&
|
|
1867
1868
|
left.quasis[1].value.raw === '') {
|
|
1868
|
-
|
|
1869
|
-
const exprText = context.sourceCode.getText(expr);
|
|
1870
|
-
reportWrittenKey(written, left, exprText);
|
|
1869
|
+
reportWrittenKey(written);
|
|
1871
1870
|
}
|
|
1872
1871
|
}
|
|
1873
1872
|
},
|
|
@@ -1909,9 +1908,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
1909
1908
|
if (property.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
1910
1909
|
property.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
1911
1910
|
property.callee.name === 'String') {
|
|
1912
|
-
|
|
1913
|
-
const argText = context.sourceCode.getText(arg);
|
|
1914
|
-
reportWrittenKey(written, property, argText);
|
|
1911
|
+
reportWrittenKey(written);
|
|
1915
1912
|
return;
|
|
1916
1913
|
}
|
|
1917
1914
|
// Check for template literals
|
|
@@ -1935,19 +1932,14 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
1935
1932
|
if (!reachesPrototype) {
|
|
1936
1933
|
return;
|
|
1937
1934
|
}
|
|
1938
|
-
//
|
|
1939
|
-
//
|
|
1940
|
-
//
|
|
1941
|
-
//
|
|
1942
|
-
//
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
property.quasis[1].value.raw === '';
|
|
1947
|
-
const unwrapped = isSimpleVarInterpolation
|
|
1948
|
-
? property.expressions[0]
|
|
1949
|
-
: property;
|
|
1950
|
-
reportWrittenKey(written, property, context.sourceCode.getText(unwrapped));
|
|
1935
|
+
// The string the template builds is the key, whether it carries
|
|
1936
|
+
// fixed text or is a lone substitution, so the whole template is
|
|
1937
|
+
// what gets wrapped — the shape the docs show for
|
|
1938
|
+
// `assertSafe(`${id}_suffix`)`. Reaching inside for `${id}` would
|
|
1939
|
+
// hand assertSafe the operand instead of the string the operand
|
|
1940
|
+
// widens to, and assertSafe throws on anything that is not already
|
|
1941
|
+
// a string or a number (#2144).
|
|
1942
|
+
reportWrittenKey(written);
|
|
1951
1943
|
return;
|
|
1952
1944
|
}
|
|
1953
1945
|
// Check for direct variable usage (identifiers)
|
|
@@ -1971,8 +1963,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
1971
1963
|
if (isCompilerBoundedLookup(node, property)) {
|
|
1972
1964
|
return;
|
|
1973
1965
|
}
|
|
1974
|
-
|
|
1975
|
-
reportWrittenKey(written, property, propText);
|
|
1966
|
+
reportWrittenKey(written);
|
|
1976
1967
|
return;
|
|
1977
1968
|
}
|
|
1978
1969
|
// Check for binary expressions (like index + 1)
|
|
@@ -1981,8 +1972,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
1981
1972
|
if (isLikelyArray) {
|
|
1982
1973
|
return;
|
|
1983
1974
|
}
|
|
1984
|
-
|
|
1985
|
-
reportWrittenKey(written, property, propText);
|
|
1975
|
+
reportWrittenKey(written);
|
|
1986
1976
|
return;
|
|
1987
1977
|
}
|
|
1988
1978
|
// Check for boolean expressions and other literals
|
|
@@ -1993,8 +1983,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
1993
1983
|
if (isLikelyArray) {
|
|
1994
1984
|
return;
|
|
1995
1985
|
}
|
|
1996
|
-
|
|
1997
|
-
reportWrittenKey(written, property, propText);
|
|
1986
|
+
reportWrittenKey(written);
|
|
1998
1987
|
return;
|
|
1999
1988
|
}
|
|
2000
1989
|
// Check for function calls (anything that isn't handled above)
|
|
@@ -2006,8 +1995,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
2006
1995
|
if (isLikelyArray) {
|
|
2007
1996
|
return;
|
|
2008
1997
|
}
|
|
2009
|
-
|
|
2010
|
-
reportWrittenKey(written, property, propText);
|
|
1998
|
+
reportWrittenKey(written);
|
|
2011
1999
|
return;
|
|
2012
2000
|
}
|
|
2013
2001
|
}
|
|
@@ -647,9 +647,9 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
647
647
|
* inline there emits text the formatter immediately re-breaks, so the fix
|
|
648
648
|
* is never a fixed point of the consumer's own formatting pass (#2097).
|
|
649
649
|
*
|
|
650
|
-
* A list written across lines answers the first half outright
|
|
651
|
-
*
|
|
652
|
-
* the
|
|
650
|
+
* A list written across lines answers the first half outright and is
|
|
651
|
+
* handled before this is asked: the caller keeps it broken, except for
|
|
652
|
+
* the block-comment tail {@link flattenedListFixes} claims (#2142).
|
|
653
653
|
*
|
|
654
654
|
* The width half is answered only where the formatter's own answer is
|
|
655
655
|
* modelled end to end. A trailing options object gets hugged against the
|
|
@@ -683,17 +683,93 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
683
683
|
const widest = Math.max(...spans.map((span) => span.end - span.start), ...appended.map((argument) => argument.length));
|
|
684
684
|
return body + widest + ','.length <= PRINT_WIDTH;
|
|
685
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* The width the call prints at with its argument list riding one line:
|
|
688
|
+
* everything on the opening line through the parenthesis, each span
|
|
689
|
+
* joined by `, `, the tail comments a written comma strands outside the
|
|
690
|
+
* last span, the appended arguments, and whatever follows the closing
|
|
691
|
+
* parenthesis on its own line.
|
|
692
|
+
*/
|
|
693
|
+
function flatListWidth(layout, appended, nameDelta, gapComments) {
|
|
694
|
+
const { openParen, closeParen, spans } = layout;
|
|
695
|
+
const lastSpanEnd = spans[spans.length - 1].end;
|
|
696
|
+
const suffix = (sourceCode.lines[closeParen.loc.start.line - 1] ?? '')
|
|
697
|
+
.slice(closeParen.loc.start.column)
|
|
698
|
+
.trimEnd();
|
|
699
|
+
return (openParen.loc.end.column +
|
|
700
|
+
nameDelta +
|
|
701
|
+
spans.reduce((width, span) => width + (span.end - span.start), 0) +
|
|
702
|
+
(spans.length - 1) * ', '.length +
|
|
703
|
+
gapComments
|
|
704
|
+
.filter((comment) => comment.range[0] >= lastSpanEnd)
|
|
705
|
+
.reduce((width, comment) => width + ' '.length + (comment.range[1] - comment.range[0]), 0) +
|
|
706
|
+
appended.reduce((width, argument) => width + ', '.length + argument.length, 0) +
|
|
707
|
+
suffix.length);
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* The edits that print an authored-broken list flat, with `appended` on
|
|
711
|
+
* its tail — or `null` where the flat layout is out of reach or out of
|
|
712
|
+
* scope.
|
|
713
|
+
*
|
|
714
|
+
* A list written across lines usually stays broken: a multi-line argument
|
|
715
|
+
* cannot be inlined without rewriting its text, and a line comment pins
|
|
716
|
+
* its break outright. A break held up by nothing but a BLOCK comment
|
|
717
|
+
* trailing the last argument is neither — a block comment is no line
|
|
718
|
+
* terminator, so the consumer's formatter collapses the whole call as
|
|
719
|
+
* soon as it fits the print width, and no broken emission is a fixed
|
|
720
|
+
* point of its formatting pass there (#2142). Only that annotated tail
|
|
721
|
+
* claims the flat layout; elsewhere the author's breaks are kept — a
|
|
722
|
+
* choice the formatter may fold, but one this fix did not create.
|
|
723
|
+
*/
|
|
724
|
+
function flattenedListFixes(fixer, node, layout, appended, nameDelta, beforeClose, gapComments, ownLineComments) {
|
|
725
|
+
const { openParen, closeParen, spans } = layout;
|
|
726
|
+
if (gapComments.length === 0 ||
|
|
727
|
+
ownLineComments.length > 0 ||
|
|
728
|
+
gapComments.some(replacementSegments_1.requiresOwnLine) ||
|
|
729
|
+
calleeBreaksFirst(node.callee) ||
|
|
730
|
+
spans.some((span) => sourceCode.text.slice(span.start, span.end).includes('\n')) ||
|
|
731
|
+
flatListWidth(layout, appended, nameDelta, gapComments) > PRINT_WIDTH) {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
const fixes = [
|
|
735
|
+
fixer.replaceTextRange([openParen.range[1], spans[0].start], ''),
|
|
736
|
+
];
|
|
737
|
+
for (let index = 1; index < spans.length; index++) {
|
|
738
|
+
fixes.push(fixer.replaceTextRange([spans[index - 1].end, spans[index].start], ', '));
|
|
739
|
+
}
|
|
740
|
+
const lastSpanEnd = spans[spans.length - 1].end;
|
|
741
|
+
const flatTail = appended.map((argument) => `, ${argument}`).join('');
|
|
742
|
+
if (beforeClose.value !== ',') {
|
|
743
|
+
// With no written comma the tail comments sit inside the last span,
|
|
744
|
+
// so everything between the span and the parenthesis is whitespace.
|
|
745
|
+
fixes.push(fixer.replaceTextRange([lastSpanEnd, closeParen.range[0]], flatTail));
|
|
746
|
+
return fixes;
|
|
747
|
+
}
|
|
748
|
+
const pastComma = gapComments.filter((comment) => comment.range[0] >= beforeClose.range[1]);
|
|
749
|
+
if (pastComma.length === 0) {
|
|
750
|
+
// The written comma already trails the annotation; it is the
|
|
751
|
+
// separator the appended arguments ride on.
|
|
752
|
+
fixes.push(fixer.replaceTextRange([beforeClose.range[1], closeParen.range[0]], ` ${appended.join(', ')}`));
|
|
753
|
+
return fixes;
|
|
754
|
+
}
|
|
755
|
+
// The written comma moves past the comments it precedes: prettier
|
|
756
|
+
// prints a block comment on a list element BEFORE the separator.
|
|
757
|
+
fixes.push(fixer.removeRange(beforeClose.range));
|
|
758
|
+
fixes.push(fixer.replaceTextRange([pastComma[pastComma.length - 1].range[1], closeParen.range[0]], flatTail));
|
|
759
|
+
return fixes;
|
|
760
|
+
}
|
|
686
761
|
/**
|
|
687
762
|
* The edits that add `appended` to the end of a call's argument list, laid
|
|
688
763
|
* out the way the consumer's formatter prints the result — or `null` for
|
|
689
764
|
* the one tail no edit can extend without retargeting a directive.
|
|
690
765
|
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
766
|
+
* No layout re-emits an argument from its text. The broken one rewrites
|
|
767
|
+
* the SEPARATORS between the arguments and shifts the indentation of the
|
|
768
|
+
* ones that span lines, and the flat one rewrites the separators alone,
|
|
769
|
+
* so everything between them — comments included, and a dropped
|
|
770
|
+
* `eslint-disable` silently re-enables the rule it was suppressing
|
|
771
|
+
* (#1877) — stays where it was written, attached to the argument it
|
|
772
|
+
* belongs to.
|
|
697
773
|
*
|
|
698
774
|
* `nameDelta` is how much the caller's own rename widens the call, since
|
|
699
775
|
* the two edits land on the same line and the width answer is about the
|
|
@@ -705,10 +781,17 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
705
781
|
fixer.insertTextAfter(args[args.length - 1], appended.map((argument) => `, ${argument}`).join('')),
|
|
706
782
|
];
|
|
707
783
|
const layout = callLayout(node);
|
|
708
|
-
if (!layout
|
|
784
|
+
if (!layout) {
|
|
709
785
|
return inline();
|
|
710
786
|
}
|
|
711
787
|
const { openParen, closeParen, spans } = layout;
|
|
788
|
+
const listSpansLines = sourceCode.text
|
|
789
|
+
.slice(openParen.range[1], closeParen.range[0])
|
|
790
|
+
.includes('\n');
|
|
791
|
+
if (!listSpansLines &&
|
|
792
|
+
!requiresBrokenList(node, layout, appended, nameDelta)) {
|
|
793
|
+
return inline();
|
|
794
|
+
}
|
|
712
795
|
// Between the last argument's own last token and the closing parenthesis
|
|
713
796
|
// sit the trailing comma, if one was written, and any comment. The tail
|
|
714
797
|
// SPAN absorbs such a comment whenever no comma follows it, so the span's
|
|
@@ -739,6 +822,12 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
739
822
|
if (sameLineComments.some(isPositionalDirective)) {
|
|
740
823
|
return null;
|
|
741
824
|
}
|
|
825
|
+
if (listSpansLines) {
|
|
826
|
+
const flattened = flattenedListFixes(fixer, node, layout, appended, nameDelta, beforeClose, gapComments, ownLineComments);
|
|
827
|
+
if (flattened) {
|
|
828
|
+
return flattened;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
742
831
|
// A broken list is indented one step past the line its parenthesis opens
|
|
743
832
|
// on, whatever depth that line sits at, and closes at that line's own
|
|
744
833
|
// column. A constant indent is right for exactly one call site.
|
|
@@ -769,18 +858,34 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
769
858
|
}
|
|
770
859
|
const separatorPresent = beforeClose.value === ',';
|
|
771
860
|
const lastSameLine = sameLineComments[sameLineComments.length - 1];
|
|
861
|
+
// WHERE the separator sits among the trailing comments is the
|
|
862
|
+
// formatter's call, not a constant: prettier prints a line comment on a
|
|
863
|
+
// list element after the comma and a block comment before it (#2142).
|
|
864
|
+
// The comma therefore lands past the leading run of block comments — at
|
|
865
|
+
// the argument's own last token when that run is empty — and a comma
|
|
866
|
+
// the author wrote on the wrong side of the run is moved rather than
|
|
867
|
+
// doubled. Everything else in the annotation is left byte for byte as
|
|
868
|
+
// written.
|
|
869
|
+
let blockRunEnd = lastArgumentToken.range[1];
|
|
870
|
+
for (const comment of sameLineComments) {
|
|
871
|
+
if ((0, replacementSegments_1.requiresLineBreakAfter)(comment)) {
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
blockRunEnd = comment.range[1];
|
|
875
|
+
}
|
|
876
|
+
const commaMisplaced = separatorPresent && beforeClose.range[1] < blockRunEnd;
|
|
877
|
+
const needsComma = !separatorPresent || commaMisplaced;
|
|
878
|
+
if (commaMisplaced) {
|
|
879
|
+
fixes.push(fixer.removeRange(beforeClose.range));
|
|
880
|
+
}
|
|
772
881
|
if (ownLineComments.length === 0) {
|
|
773
882
|
// The comment trails the argument it annotates, so it keeps that line
|
|
774
|
-
// and the option opens the next one.
|
|
775
|
-
// the argument and the comment — after a line comment it is comment
|
|
776
|
-
// text — so one is inserted at the argument's own last token unless
|
|
777
|
-
// the author already wrote it, and everything through the end of the
|
|
778
|
-
// annotation is left byte for byte as written.
|
|
779
|
-
if (!separatorPresent) {
|
|
780
|
-
fixes.push(fixer.insertTextAfter(lastArgumentToken, ','));
|
|
781
|
-
}
|
|
883
|
+
// and the option opens the next one.
|
|
782
884
|
const annotationEnd = Math.max(lastSameLine.range[1], beforeClose.range[1]);
|
|
783
|
-
|
|
885
|
+
if (needsComma && blockRunEnd < annotationEnd) {
|
|
886
|
+
fixes.push(fixer.insertTextAfterRange([blockRunEnd, blockRunEnd], ','));
|
|
887
|
+
}
|
|
888
|
+
fixes.push(fixer.replaceTextRange([annotationEnd, closeParen.range[0]], `${needsComma && blockRunEnd >= annotationEnd ? ',' : ''}${appended
|
|
784
889
|
.map((argument) => `\n${body}${argument},`)
|
|
785
890
|
.join('')}\n${indent}`));
|
|
786
891
|
return fixes;
|
|
@@ -792,16 +897,20 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
792
897
|
const appendedText = appended
|
|
793
898
|
.map((argument) => `\n${body}${argument},`)
|
|
794
899
|
.join('');
|
|
795
|
-
const anchor = Math.max(separatorPresent
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
900
|
+
const anchor = Math.max(separatorPresent && !commaMisplaced
|
|
901
|
+
? beforeClose.range[1]
|
|
902
|
+
: lastArgumentToken.range[1], lastSameLine ? lastSameLine.range[1] : 0);
|
|
903
|
+
if (!needsComma) {
|
|
904
|
+
fixes.push(fixer.insertTextAfterRange([anchor, anchor], appendedText));
|
|
905
|
+
return fixes;
|
|
906
|
+
}
|
|
907
|
+
if (blockRunEnd < anchor) {
|
|
908
|
+
fixes.push(fixer.insertTextAfterRange([blockRunEnd, blockRunEnd], ','));
|
|
800
909
|
fixes.push(fixer.insertTextAfterRange([anchor, anchor], appendedText));
|
|
801
910
|
return fixes;
|
|
802
911
|
}
|
|
803
|
-
// With
|
|
804
|
-
//
|
|
912
|
+
// With the separator due at the annotation's own end, it and the option
|
|
913
|
+
// travel as one insertion, since both land at the same offset.
|
|
805
914
|
fixes.push(fixer.insertTextAfterRange([anchor, anchor], `,${appendedText}`));
|
|
806
915
|
return fixes;
|
|
807
916
|
}
|
|
@@ -41,6 +41,141 @@ const NON_PLURALIZABLE_SUFFIXES = [
|
|
|
41
41
|
* the reference itself as a container.
|
|
42
42
|
*/
|
|
43
43
|
const ARRAY_GENERIC_NAMES = new Set(['Array', 'ReadonlyArray']);
|
|
44
|
+
/**
|
|
45
|
+
* Word-final suffixes that are singular by shape. No English plural ends in
|
|
46
|
+
* `-sis` or `-ss`, so both classes are exempt wholesale rather than enumerated:
|
|
47
|
+
* every `-sis` noun (`Analysis`, `Basis`, `Thesis`, `Diagnosis`, `Synopsis`,
|
|
48
|
+
* `Chassis`) and every `-ss` noun (`Address`, `Progress`, `Class`, `Success`)
|
|
49
|
+
* is covered, including ones no list would anticipate.
|
|
50
|
+
*
|
|
51
|
+
* The neighbouring `-is`/`-us`/`-os` classes get no such blanket rule, because
|
|
52
|
+
* genuine plurals do end that way — `Emojis`, `Minis`, `Menus`, `Plateaus`,
|
|
53
|
+
* `Taxis`. Exempting those by shape would silence the rule on real collection
|
|
54
|
+
* names, so they are enumerated in SINGULAR_NOUNS_ENDING_IN_S instead.
|
|
55
|
+
*/
|
|
56
|
+
const SINGULAR_WORD_SUFFIXES = ['sis', 'ss'];
|
|
57
|
+
/**
|
|
58
|
+
* Singular nouns that end in `s` and that `pluralize` mistakes for plurals.
|
|
59
|
+
* Stripping the trailing `s` yields a non-word — `Axis` → `Axi`, `Lens` →
|
|
60
|
+
* `Len`, `Chaos` → `Chao` — which is the tell that the identifier was singular
|
|
61
|
+
* all along. Enumeration is the only safe discriminator here: `Axis` and
|
|
62
|
+
* `Minis` share a shape, so nothing but the noun itself separates a Latin/Greek
|
|
63
|
+
* singular from an ordinary plural.
|
|
64
|
+
*
|
|
65
|
+
* Entries `pluralize` already classifies correctly (`Status`, `Corpus`) are
|
|
66
|
+
* listed too, so a change in that library's heuristics cannot quietly
|
|
67
|
+
* reintroduce the false positive.
|
|
68
|
+
*/
|
|
69
|
+
const SINGULAR_NOUNS_ENDING_IN_S = new Set([
|
|
70
|
+
// Latin/Greek `-is` singulars outside the `-sis` family above.
|
|
71
|
+
'axis',
|
|
72
|
+
'praxis',
|
|
73
|
+
'prophylaxis',
|
|
74
|
+
'aegis',
|
|
75
|
+
'cannabis',
|
|
76
|
+
'chrysalis',
|
|
77
|
+
'clematis',
|
|
78
|
+
'dais',
|
|
79
|
+
'dermis',
|
|
80
|
+
'epidermis',
|
|
81
|
+
'epiglottis',
|
|
82
|
+
'glottis',
|
|
83
|
+
'hubris',
|
|
84
|
+
'ibis',
|
|
85
|
+
'iris',
|
|
86
|
+
'mantis',
|
|
87
|
+
'marquis',
|
|
88
|
+
'megalopolis',
|
|
89
|
+
'metropolis',
|
|
90
|
+
'pelvis',
|
|
91
|
+
'portcullis',
|
|
92
|
+
'proboscis',
|
|
93
|
+
'tennis',
|
|
94
|
+
'trellis',
|
|
95
|
+
// `-us` singulars.
|
|
96
|
+
'alumnus',
|
|
97
|
+
'apparatus',
|
|
98
|
+
'bonus',
|
|
99
|
+
'bus',
|
|
100
|
+
'cactus',
|
|
101
|
+
'campus',
|
|
102
|
+
'census',
|
|
103
|
+
'chorus',
|
|
104
|
+
'consensus',
|
|
105
|
+
'corpus',
|
|
106
|
+
'exodus',
|
|
107
|
+
'focus',
|
|
108
|
+
'fungus',
|
|
109
|
+
'genus',
|
|
110
|
+
'hiatus',
|
|
111
|
+
'impetus',
|
|
112
|
+
'locus',
|
|
113
|
+
'minus',
|
|
114
|
+
'modulus',
|
|
115
|
+
'nexus',
|
|
116
|
+
'nucleus',
|
|
117
|
+
'octopus',
|
|
118
|
+
'opus',
|
|
119
|
+
'plus',
|
|
120
|
+
'prospectus',
|
|
121
|
+
'radius',
|
|
122
|
+
'sinus',
|
|
123
|
+
'status',
|
|
124
|
+
'stimulus',
|
|
125
|
+
'surplus',
|
|
126
|
+
'syllabus',
|
|
127
|
+
'terminus',
|
|
128
|
+
'thesaurus',
|
|
129
|
+
'versus',
|
|
130
|
+
'virus',
|
|
131
|
+
// Remaining singular nouns ending in `s`.
|
|
132
|
+
'alias',
|
|
133
|
+
'apropos',
|
|
134
|
+
'asbestos',
|
|
135
|
+
'atlas',
|
|
136
|
+
'bias',
|
|
137
|
+
'canvas',
|
|
138
|
+
'chaos',
|
|
139
|
+
'cosmos',
|
|
140
|
+
'ethos',
|
|
141
|
+
'fracas',
|
|
142
|
+
'gas',
|
|
143
|
+
'kudos',
|
|
144
|
+
'lens',
|
|
145
|
+
'news',
|
|
146
|
+
'pancreas',
|
|
147
|
+
'pathos',
|
|
148
|
+
'rhinoceros',
|
|
149
|
+
'series',
|
|
150
|
+
'species',
|
|
151
|
+
'thermos',
|
|
152
|
+
]);
|
|
153
|
+
/**
|
|
154
|
+
* Splits a PascalCase/camelCase identifier into its words, keeping an
|
|
155
|
+
* all-caps run together: `DeferAxis` → `Defer`/`Axis`, `HTTPStatus` →
|
|
156
|
+
* `HTTP`/`Status`.
|
|
157
|
+
*/
|
|
158
|
+
const IDENTIFIER_WORD_PATTERN = /[A-Z]+(?![a-z])|[A-Z]?[a-z0-9]+/g;
|
|
159
|
+
/**
|
|
160
|
+
* The exemption keys on the identifier's FINAL word rather than the whole
|
|
161
|
+
* identifier so it composes with any prefix: `Axis`, `DeferAxis` and
|
|
162
|
+
* `ChartRenderAxis` all resolve to `axis`. Whole-identifier matching would
|
|
163
|
+
* exempt only the bare noun and keep reporting every compound built on it.
|
|
164
|
+
*/
|
|
165
|
+
function trailingWordOf(name) {
|
|
166
|
+
const words = name.match(IDENTIFIER_WORD_PATTERN);
|
|
167
|
+
return (words?.[words.length - 1] ?? name).toLowerCase();
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* True when the identifier's final word is a singular noun that merely ends in
|
|
171
|
+
* `s`. Checked before `pluralize`, whose naive trailing-`s` strip is what
|
|
172
|
+
* misreads these nouns in the first place.
|
|
173
|
+
*/
|
|
174
|
+
function endsWithSingularNoun(name) {
|
|
175
|
+
const trailingWord = trailingWordOf(name);
|
|
176
|
+
return (SINGULAR_NOUNS_ENDING_IN_S.has(trailingWord) ||
|
|
177
|
+
SINGULAR_WORD_SUFFIXES.some((suffix) => trailingWord.endsWith(suffix)));
|
|
178
|
+
}
|
|
44
179
|
/**
|
|
45
180
|
* Union members that only express absence. Stripping them keeps a nullable
|
|
46
181
|
* container recognisable as a container: `T[]` and `T[] | null` describe the
|
|
@@ -137,6 +272,11 @@ exports.enforceSingularTypeNames = (0, createRule_1.createRule)({
|
|
|
137
272
|
// Skip checking if name ends with 'Props', 'Params', 'Data', etc.
|
|
138
273
|
if (NON_PLURALIZABLE_SUFFIXES.some((suffix) => name.toLowerCase().endsWith(suffix.toLowerCase())))
|
|
139
274
|
return false;
|
|
275
|
+
// Singular nouns that end in `s` must be settled BEFORE pluralize sees
|
|
276
|
+
// them: its trailing-`s` strip turns `DeferAxis` into the non-word
|
|
277
|
+
// `DeferAxi` and reports a rename to it.
|
|
278
|
+
if (endsWithSingularNoun(name))
|
|
279
|
+
return false;
|
|
140
280
|
// Skip checking if name is already singular according to pluralize
|
|
141
281
|
if (pluralize.isSingular(name))
|
|
142
282
|
return false;
|
|
@@ -883,14 +883,19 @@ function chainComments(node, sourceCode, operands) {
|
|
|
883
883
|
* inside an operand's own brackets — a line comment in an object literal passed
|
|
884
884
|
* to a call — belongs to that operand's layout and leaves the chain's own layout
|
|
885
885
|
* alone, which is how prettier prints it.
|
|
886
|
+
*
|
|
887
|
+
* `absorbed` holds the comments the rewrite carries out past the statement's
|
|
888
|
+
* closing punctuator: those sit in no gap of the emitted chain, so counting one
|
|
889
|
+
* as a break would split a group that ends up holding no comment at all
|
|
890
|
+
* (#2141).
|
|
886
891
|
*/
|
|
887
|
-
function chainBreaksLine(node, sourceCode) {
|
|
892
|
+
function chainBreaksLine(node, sourceCode, absorbed) {
|
|
888
893
|
if (node.type !== utils_1.AST_NODE_TYPES.LogicalExpression) {
|
|
889
894
|
return false;
|
|
890
895
|
}
|
|
891
|
-
return (strandedComments(node, sourceCode).some(replacementSegments_1.requiresLineBreakAfter) ||
|
|
892
|
-
chainBreaksLine(node.left, sourceCode) ||
|
|
893
|
-
chainBreaksLine(node.right, sourceCode));
|
|
896
|
+
return (strandedComments(node, sourceCode).some((comment) => !absorbed.has(comment) && (0, replacementSegments_1.requiresLineBreakAfter)(comment)) ||
|
|
897
|
+
chainBreaksLine(node.left, sourceCode, absorbed) ||
|
|
898
|
+
chainBreaksLine(node.right, sourceCode, absorbed));
|
|
894
899
|
}
|
|
895
900
|
/**
|
|
896
901
|
* Whether the chain a link belongs to breaks the line, asked of the whole chain
|
|
@@ -902,12 +907,16 @@ function chainBreaksLine(node, sourceCode) {
|
|
|
902
907
|
* broken packs two operands onto a line the formatter then splits (#2106).
|
|
903
908
|
*
|
|
904
909
|
* A comment trailing the chain, or leading it, is outside every gap: prettier
|
|
905
|
-
* prints it on the line the chain already occupies and moves no operand.
|
|
910
|
+
* prints it on the line the chain already occupies and moves no operand. The
|
|
911
|
+
* same goes for a comment in `absorbed` — one the fix carries out past the
|
|
912
|
+
* statement's closing punctuator: wherever the INPUT held it, the emission
|
|
913
|
+
* holds it behind the statement, outside every gap (#2141).
|
|
906
914
|
*/
|
|
907
|
-
function chainBreaksAtAGap(chain, sourceCode) {
|
|
915
|
+
function chainBreaksAtAGap(chain, sourceCode, absorbed) {
|
|
908
916
|
const operands = chainOperands(chain);
|
|
909
917
|
const { gaps } = chainComments(chain, sourceCode, operands);
|
|
910
|
-
return (gaps.some((gap) => [...gap.beforeOperator, ...gap.afterOperator].some(
|
|
918
|
+
return (gaps.some((gap) => [...gap.beforeOperator, ...gap.afterOperator].some((comment) => !absorbed.has(comment) && (0, replacementSegments_1.requiresLineBreakAfter)(comment))) ||
|
|
919
|
+
operands.some((operand) => chainBreaksLine(operand, sourceCode, absorbed)));
|
|
911
920
|
}
|
|
912
921
|
/**
|
|
913
922
|
* The depths the rebuilt expression lands at.
|
|
@@ -1196,6 +1205,36 @@ exports.preferNullishCoalescingBooleanProps = (0, createRule_1.createRule)({
|
|
|
1196
1205
|
});
|
|
1197
1206
|
});
|
|
1198
1207
|
segments.push(...comments.trailing.map(toSegment));
|
|
1208
|
+
// A comment trailing the whole expression rides out past the
|
|
1209
|
+
// punctuator that closes the statement the chain stands in,
|
|
1210
|
+
// where one is available to take over. Kept inside the
|
|
1211
|
+
// replacement, the line break such a comment demands strands
|
|
1212
|
+
// that punctuator on a line of its own — layout a formatter
|
|
1213
|
+
// folds straight back, and the fold moves an
|
|
1214
|
+
// `eslint-disable-next-line` written on the comment's line
|
|
1215
|
+
// onto a different subject, so the pipeline's formatting order
|
|
1216
|
+
// decides which violations are enforced (#2139). The span this
|
|
1217
|
+
// fix replaces can end PAST the node, at a redundant paren the
|
|
1218
|
+
// widening claims, so the lookup anchors on that paren where
|
|
1219
|
+
// the widening applies: asked of the node, it answers with the
|
|
1220
|
+
// paren — a token inside the span — instead of the punctuator
|
|
1221
|
+
// behind it. The landing widening reaches BACKWARD from the
|
|
1222
|
+
// node and never past its end, so everywhere else the span
|
|
1223
|
+
// ends at the node's own last token. The parenthesized
|
|
1224
|
+
// (selfParens) replacement keeps every comment inside the
|
|
1225
|
+
// emitted parens, where no punctuator can be stranded.
|
|
1226
|
+
const spanEndToken = redundantParens(node, sourceCode)
|
|
1227
|
+
? sourceCode.getTokenAfter(node)
|
|
1228
|
+
: sourceCode.getLastToken(node);
|
|
1229
|
+
const closingPunctuator = !selfParens && comments.trailing.length > 0 && spanEndToken
|
|
1230
|
+
? (0, replacementSegments_1.absorbableClosingPunctuator)(sourceCode, spanEndToken, comments.trailing)
|
|
1231
|
+
: null;
|
|
1232
|
+
// The absorption is decided BEFORE the break decision because
|
|
1233
|
+
// a comment carried past the punctuator sits in no gap of the
|
|
1234
|
+
// emitted chain: counted as a break it would put every operand
|
|
1235
|
+
// on a line of its own for a comment that no longer sits
|
|
1236
|
+
// between any of them (#2141).
|
|
1237
|
+
const absorbed = new Set(closingPunctuator ? comments.trailing : []);
|
|
1199
1238
|
// Prettier prints a logical chain as ONE group: once anything
|
|
1200
1239
|
// inside it breaks the line, EVERY operand takes a line of its
|
|
1201
1240
|
// own. So a chain broken by a comment anywhere gets a break in
|
|
@@ -1204,7 +1243,7 @@ exports.preferNullishCoalescingBooleanProps = (0, createRule_1.createRule)({
|
|
|
1204
1243
|
// The break rides on the last thing in the gap so a carried
|
|
1205
1244
|
// comment keeps the line it was written on, and a gap already
|
|
1206
1245
|
// holding a break needs no second separator.
|
|
1207
|
-
const chainBreaks = chainBreaksAtAGap(chainRootOf(node), sourceCode);
|
|
1246
|
+
const chainBreaks = chainBreaksAtAGap(chainRootOf(node), sourceCode, absorbed);
|
|
1208
1247
|
if (chainBreaks) {
|
|
1209
1248
|
for (const { start, end } of gapSpans) {
|
|
1210
1249
|
const separated = segments
|
|
@@ -1236,26 +1275,6 @@ exports.preferNullishCoalescingBooleanProps = (0, createRule_1.createRule)({
|
|
|
1236
1275
|
: null;
|
|
1237
1276
|
const leading = landing ? `\n${bodyIndent}` : '';
|
|
1238
1277
|
const range = replacementRange(node, sourceCode, landing);
|
|
1239
|
-
// A comment trailing the whole expression rides out past the
|
|
1240
|
-
// punctuator that closes the statement the chain stands in,
|
|
1241
|
-
// where one is available to take over. Kept inside the
|
|
1242
|
-
// replacement, the line break such a comment demands strands
|
|
1243
|
-
// that punctuator on a line of its own — layout a formatter
|
|
1244
|
-
// folds straight back, and the fold moves an
|
|
1245
|
-
// `eslint-disable-next-line` written on the comment's line
|
|
1246
|
-
// onto a different subject, so the pipeline's formatting order
|
|
1247
|
-
// decides which violations are enforced (#2139). The span this
|
|
1248
|
-
// fix replaces can end PAST the node, at a redundant paren the
|
|
1249
|
-
// widening claims, so the lookup anchors on the span's own
|
|
1250
|
-
// last token: asked of the node, it answers with that paren —
|
|
1251
|
-
// a token inside the span — instead of the punctuator behind
|
|
1252
|
-
// it.
|
|
1253
|
-
const spanEndToken = range[1] > node.range[1]
|
|
1254
|
-
? sourceCode.getTokenAfter(node)
|
|
1255
|
-
: sourceCode.getLastToken(node);
|
|
1256
|
-
const closingPunctuator = comments.trailing.length > 0 && spanEndToken
|
|
1257
|
-
? (0, replacementSegments_1.absorbableClosingPunctuator)(sourceCode, spanEndToken, comments.trailing)
|
|
1258
|
-
: null;
|
|
1259
1278
|
const bodySegments = closingPunctuator
|
|
1260
1279
|
? segments.slice(0, segments.length - comments.trailing.length)
|
|
1261
1280
|
: segments;
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,48 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.176",
|
|
4
|
+
"date": "2026-08-26T11:56:32.903Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-assert-safe-object-key",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2144
|
|
11
|
+
],
|
|
12
|
+
"summary": "wrap a coerced key instead of replacing it (closes #2144)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "enforce-singular-type-names",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
2143
|
|
19
|
+
],
|
|
20
|
+
"summary": "exempt singular nouns that merely end in s (closes #2143)"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"version": "1.20.175",
|
|
26
|
+
"date": "2026-08-26T06:19:14.998Z",
|
|
27
|
+
"rules": [
|
|
28
|
+
{
|
|
29
|
+
"name": "enforce-firestore-set-merge",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
2142
|
|
33
|
+
],
|
|
34
|
+
"summary": "place a block comment before the separator and fold a list that fits (closes #2142)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "prefer-nullish-coalescing-boolean-props",
|
|
38
|
+
"changeType": "fix",
|
|
39
|
+
"issues": [
|
|
40
|
+
2141
|
|
41
|
+
],
|
|
42
|
+
"summary": "discount an absorbed comment from the chain-break decision (closes #2141)"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
2
46
|
{
|
|
3
47
|
"version": "1.20.174",
|
|
4
48
|
"date": "2026-08-26T04:47:21.242Z",
|