@saasmakers/eslint 1.0.25 → 1.0.27
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/dist/index.cjs +264 -11
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +264 -11
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -63,7 +63,7 @@ function checkUnion(context, node, maxLineLength, minItems) {
|
|
|
63
63
|
function collapseBlankLine(text) {
|
|
64
64
|
return text.replace(/\n[^\S\n]*\n[^\S\n]*/, "\n");
|
|
65
65
|
}
|
|
66
|
-
function expressionStartsWithIdentifier(node, identifier) {
|
|
66
|
+
function expressionStartsWithIdentifier$1(node, identifier) {
|
|
67
67
|
if (node.type === index$1.distExports.AST_NODE_TYPES.Identifier && node.name === identifier) {
|
|
68
68
|
return true;
|
|
69
69
|
}
|
|
@@ -73,17 +73,17 @@ function expressionStartsWithIdentifier(node, identifier) {
|
|
|
73
73
|
return true;
|
|
74
74
|
}
|
|
75
75
|
if (callee.type === index$1.distExports.AST_NODE_TYPES.MemberExpression) {
|
|
76
|
-
return expressionStartsWithIdentifier(callee.object, identifier);
|
|
76
|
+
return expressionStartsWithIdentifier$1(callee.object, identifier);
|
|
77
77
|
}
|
|
78
78
|
if (callee.type === index$1.distExports.AST_NODE_TYPES.ChainExpression) {
|
|
79
|
-
return expressionStartsWithIdentifier(callee.expression, identifier);
|
|
79
|
+
return expressionStartsWithIdentifier$1(callee.expression, identifier);
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
if (node.type === index$1.distExports.AST_NODE_TYPES.MemberExpression) {
|
|
83
|
-
return expressionStartsWithIdentifier(node.object, identifier);
|
|
83
|
+
return expressionStartsWithIdentifier$1(node.object, identifier);
|
|
84
84
|
}
|
|
85
85
|
if (node.type === index$1.distExports.AST_NODE_TYPES.ChainExpression) {
|
|
86
|
-
return expressionStartsWithIdentifier(node.expression, identifier);
|
|
86
|
+
return expressionStartsWithIdentifier$1(node.expression, identifier);
|
|
87
87
|
}
|
|
88
88
|
return false;
|
|
89
89
|
}
|
|
@@ -211,10 +211,10 @@ function hasLineCommentBetween(prevNode, nextNode, sourceCode) {
|
|
|
211
211
|
});
|
|
212
212
|
}
|
|
213
213
|
function isAssertExpression(expression) {
|
|
214
|
-
return expressionStartsWithIdentifier(expression, "assert");
|
|
214
|
+
return expressionStartsWithIdentifier$1(expression, "assert");
|
|
215
215
|
}
|
|
216
216
|
function isConsoleExpression(expression) {
|
|
217
|
-
return expressionStartsWithIdentifier(expression, "console");
|
|
217
|
+
return expressionStartsWithIdentifier$1(expression, "console");
|
|
218
218
|
}
|
|
219
219
|
function isDirectivePrologue(node) {
|
|
220
220
|
if (node.expression.type !== index$1.distExports.AST_NODE_TYPES.Literal) {
|
|
@@ -223,7 +223,7 @@ function isDirectivePrologue(node) {
|
|
|
223
223
|
return typeof node.expression.value === "string";
|
|
224
224
|
}
|
|
225
225
|
function isExpectExpression(expression) {
|
|
226
|
-
return expressionStartsWithIdentifier(expression, "expect");
|
|
226
|
+
return expressionStartsWithIdentifier$1(expression, "expect");
|
|
227
227
|
}
|
|
228
228
|
function isOwnLineComment(comment, sourceCode) {
|
|
229
229
|
if (comment.type !== "Line") {
|
|
@@ -403,6 +403,7 @@ const rule$4 = {
|
|
|
403
403
|
}
|
|
404
404
|
};
|
|
405
405
|
|
|
406
|
+
const teardownViMethods = /* @__PURE__ */ new Set(["restoreAllMocks", "unstubAllGlobals", "useRealTimers"]);
|
|
406
407
|
function compareTests(testA, testB) {
|
|
407
408
|
const functionA = getFunctionName(testA);
|
|
408
409
|
const functionB = getFunctionName(testB);
|
|
@@ -418,6 +419,40 @@ function compareTests(testA, testB) {
|
|
|
418
419
|
const specificNameB = getSpecificName(testB);
|
|
419
420
|
return specificNameA.localeCompare(specificNameB);
|
|
420
421
|
}
|
|
422
|
+
function expressionStartsWithIdentifier(node, identifier) {
|
|
423
|
+
if (node.type === index$1.distExports.AST_NODE_TYPES.Identifier && node.name === identifier) {
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
if (node.type === index$1.distExports.AST_NODE_TYPES.CallExpression) {
|
|
427
|
+
const { callee } = node;
|
|
428
|
+
if (callee.type === index$1.distExports.AST_NODE_TYPES.Identifier && callee.name === identifier) {
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
if (callee.type === index$1.distExports.AST_NODE_TYPES.MemberExpression) {
|
|
432
|
+
return expressionStartsWithIdentifier(callee.object, identifier);
|
|
433
|
+
}
|
|
434
|
+
if (callee.type === index$1.distExports.AST_NODE_TYPES.ChainExpression) {
|
|
435
|
+
return expressionStartsWithIdentifier(callee.expression, identifier);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (node.type === index$1.distExports.AST_NODE_TYPES.MemberExpression) {
|
|
439
|
+
return expressionStartsWithIdentifier(node.object, identifier);
|
|
440
|
+
}
|
|
441
|
+
if (node.type === index$1.distExports.AST_NODE_TYPES.ChainExpression) {
|
|
442
|
+
return expressionStartsWithIdentifier(node.expression, identifier);
|
|
443
|
+
}
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
function getExpressionFromStatement(statement) {
|
|
447
|
+
if (statement.type !== index$1.distExports.AST_NODE_TYPES.ExpressionStatement) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
let { expression } = statement;
|
|
451
|
+
if (expression.type === index$1.distExports.AST_NODE_TYPES.AwaitExpression) {
|
|
452
|
+
expression = expression.argument;
|
|
453
|
+
}
|
|
454
|
+
return expression;
|
|
455
|
+
}
|
|
421
456
|
function getFunctionName(testName) {
|
|
422
457
|
const parts = testName.split(".");
|
|
423
458
|
return parts.slice(0, 2).join(".");
|
|
@@ -431,6 +466,20 @@ function getSpecificName(testName) {
|
|
|
431
466
|
}
|
|
432
467
|
return parts.slice(specialIndex + 1).join(".");
|
|
433
468
|
}
|
|
469
|
+
function getTestCallbackBlockBody(node) {
|
|
470
|
+
const callback = node.arguments[1];
|
|
471
|
+
if (!callback) {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
if (callback.type !== index$1.distExports.AST_NODE_TYPES.FunctionExpression && callback.type !== index$1.distExports.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
const { body } = callback;
|
|
478
|
+
if (body.type !== index$1.distExports.AST_NODE_TYPES.BlockStatement) {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
return body;
|
|
482
|
+
}
|
|
434
483
|
function getTestName(node) {
|
|
435
484
|
const firstArgument = node.arguments[0];
|
|
436
485
|
if (firstArgument?.type === index$1.distExports.AST_NODE_TYPES.Literal && typeof firstArgument.value === "string") {
|
|
@@ -451,12 +500,78 @@ function getTestPriority(testName) {
|
|
|
451
500
|
return 1;
|
|
452
501
|
}
|
|
453
502
|
}
|
|
503
|
+
function hasAssertMethodCall(expression) {
|
|
504
|
+
if (expression.type !== index$1.distExports.AST_NODE_TYPES.CallExpression) {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
const { callee } = expression;
|
|
508
|
+
if (callee.type === index$1.distExports.AST_NODE_TYPES.MemberExpression && callee.property.type === index$1.distExports.AST_NODE_TYPES.Identifier && callee.property.name.startsWith("assert")) {
|
|
509
|
+
if (callee.object.type === index$1.distExports.AST_NODE_TYPES.Identifier && callee.object.name === "expect") {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
function isAssertionCountExpression(expression) {
|
|
517
|
+
if (expression.type !== index$1.distExports.AST_NODE_TYPES.CallExpression) {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
const { callee } = expression;
|
|
521
|
+
if (callee.type !== index$1.distExports.AST_NODE_TYPES.MemberExpression || callee.object.type !== index$1.distExports.AST_NODE_TYPES.Identifier || callee.property.type !== index$1.distExports.AST_NODE_TYPES.Identifier) {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
if (callee.object.name === "expect" && (callee.property.name === "assertions" || callee.property.name === "hasAssertions")) {
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
return callee.object.name === "assert" && callee.property.name === "plan";
|
|
528
|
+
}
|
|
529
|
+
function isAssertionCountStatement(statement) {
|
|
530
|
+
const expression = getExpressionFromStatement(statement);
|
|
531
|
+
if (!expression) {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
return isAssertionCountExpression(expression);
|
|
535
|
+
}
|
|
536
|
+
function isAssertionExpression(expression) {
|
|
537
|
+
if (isAssertionCountExpression(expression)) {
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
if (expressionStartsWithIdentifier(expression, "assert") || expressionStartsWithIdentifier(expression, "expect")) {
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
return hasAssertMethodCall(expression);
|
|
544
|
+
}
|
|
545
|
+
function isAssertionStatement(statement) {
|
|
546
|
+
const expression = getExpressionFromStatement(statement);
|
|
547
|
+
if (!expression) {
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
return isAssertionExpression(expression);
|
|
551
|
+
}
|
|
552
|
+
function isTeardownStatement(statement) {
|
|
553
|
+
const expression = getExpressionFromStatement(statement);
|
|
554
|
+
if (!expression || expression.type !== index$1.distExports.AST_NODE_TYPES.CallExpression) {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
const { callee } = expression;
|
|
558
|
+
if (callee.type === index$1.distExports.AST_NODE_TYPES.Identifier && callee.name === "cleanup") {
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
if (callee.type === index$1.distExports.AST_NODE_TYPES.MemberExpression && callee.object.type === index$1.distExports.AST_NODE_TYPES.Identifier && callee.object.name === "vi" && callee.property.type === index$1.distExports.AST_NODE_TYPES.Identifier && teardownViMethods.has(callee.property.name)) {
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
454
566
|
const rule$3 = {
|
|
455
567
|
defaultOptions: [],
|
|
456
568
|
meta: {
|
|
457
|
-
docs: { description: "Enforce sorted test functions
|
|
569
|
+
docs: { description: "Enforce sorted test functions and setup-before-assertions layout in test bodies" },
|
|
458
570
|
fixable: "code",
|
|
459
|
-
messages: {
|
|
571
|
+
messages: {
|
|
572
|
+
setupAfterAssertion: "Setup statements must appear before assertions in test bodies.",
|
|
573
|
+
sortError: "Test functions should be grouped by method with sorted metrics, errors, exceptions and middlewares."
|
|
574
|
+
},
|
|
460
575
|
schema: [],
|
|
461
576
|
type: "suggestion"
|
|
462
577
|
},
|
|
@@ -464,6 +579,25 @@ const rule$3 = {
|
|
|
464
579
|
if (!context.filename.endsWith(".spec.ts") && !context.filename.endsWith(".test.ts")) {
|
|
465
580
|
return {};
|
|
466
581
|
}
|
|
582
|
+
function handleTestBody(node) {
|
|
583
|
+
const callbackBody = getTestCallbackBlockBody(node);
|
|
584
|
+
if (!callbackBody) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const firstAssertionIndex = callbackBody.body.findIndex(isAssertionStatement);
|
|
588
|
+
if (firstAssertionIndex === -1) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
for (const statement of callbackBody.body.slice(firstAssertionIndex + 1)) {
|
|
592
|
+
if (isAssertionStatement(statement) || isAssertionCountStatement(statement) || isTeardownStatement(statement)) {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
context.report({
|
|
596
|
+
messageId: "setupAfterAssertion",
|
|
597
|
+
node: statement
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
467
601
|
function handleTestGroup(node) {
|
|
468
602
|
const testGroup = node.arguments[0];
|
|
469
603
|
if (!testGroup || !node.arguments[1]) {
|
|
@@ -512,6 +646,8 @@ const rule$3 = {
|
|
|
512
646
|
}
|
|
513
647
|
return {
|
|
514
648
|
'CallExpression[callee.name="describe"]': handleTestGroup,
|
|
649
|
+
'CallExpression[callee.name="it"]': handleTestBody,
|
|
650
|
+
'CallExpression[callee.name="test"]': handleTestBody,
|
|
515
651
|
'CallExpression[callee.object.name="describe"][callee.property.name="concurrent"]': handleTestGroup,
|
|
516
652
|
'CallExpression[callee.object.name="test"][callee.property.name="group"]': handleTestGroup
|
|
517
653
|
};
|
|
@@ -810,9 +946,61 @@ const propsPrefixRegex = /\$?props\.(\w+)/g;
|
|
|
810
946
|
const trueAttributeRegex = /(?<![\w-]):?(?!aria-)([a-z0-9-]+)="true"/gi;
|
|
811
947
|
const eventHandlerRegex = /(?:@|v-on:)[^\s="'<>/]+\s*=\s*"([^"]*)"/g;
|
|
812
948
|
const bareIdentifierRegex = /^[a-z_$][\w$]*$/i;
|
|
949
|
+
const classBindingRegex = /(?<![\w-])(?::|v-bind:)class\s*=\s*"([^"]*)"/g;
|
|
950
|
+
const simpleTernaryRegex = /^([^?]+)\?\s*'([^']*)'\s*:\s*'([^']*)'$/;
|
|
813
951
|
const singleCallRegex = /^([a-z_$][\w$]*)\s*\(.*\)$/is;
|
|
814
952
|
const onPrefixRegex = /^on[A-Z]/;
|
|
815
953
|
const exemptCallees = /* @__PURE__ */ new Set(["$emit", "emit"]);
|
|
954
|
+
function splitClassObjectProperties(body) {
|
|
955
|
+
const properties = [];
|
|
956
|
+
let depth = 0;
|
|
957
|
+
let quote;
|
|
958
|
+
let current = "";
|
|
959
|
+
for (let index = 0; index < body.length; index++) {
|
|
960
|
+
const char = body[index];
|
|
961
|
+
if (quote !== void 0) {
|
|
962
|
+
current += char;
|
|
963
|
+
if (char === quote && body[index - 1] !== "\\") {
|
|
964
|
+
quote = void 0;
|
|
965
|
+
}
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
969
|
+
quote = char;
|
|
970
|
+
current += char;
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
if (char === "(" || char === "[" || char === "{") {
|
|
974
|
+
depth++;
|
|
975
|
+
} else if (char === ")" || char === "]" || char === "}") {
|
|
976
|
+
depth--;
|
|
977
|
+
}
|
|
978
|
+
if (char === "," && depth === 0) {
|
|
979
|
+
const property2 = current.trim();
|
|
980
|
+
if (property2 !== "") {
|
|
981
|
+
properties.push(property2);
|
|
982
|
+
}
|
|
983
|
+
current = "";
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
current += char;
|
|
987
|
+
}
|
|
988
|
+
const property = current.trim();
|
|
989
|
+
if (property !== "") {
|
|
990
|
+
properties.push(property);
|
|
991
|
+
}
|
|
992
|
+
return properties;
|
|
993
|
+
}
|
|
994
|
+
function buildClassObjectText(properties, baseIndent) {
|
|
995
|
+
if (properties.length <= 1) {
|
|
996
|
+
return `{ ${properties.join(", ")} }`;
|
|
997
|
+
}
|
|
998
|
+
const propertyIndent = `${baseIndent} `;
|
|
999
|
+
const lines = properties.map((property) => `${propertyIndent}${property},`);
|
|
1000
|
+
return `{
|
|
1001
|
+
${lines.join("\n")}
|
|
1002
|
+
${baseIndent}}`;
|
|
1003
|
+
}
|
|
816
1004
|
const dollarTPatterns = [
|
|
817
1005
|
{
|
|
818
1006
|
pattern: / \$t\(/g,
|
|
@@ -838,13 +1026,16 @@ const dollarTPatterns = [
|
|
|
838
1026
|
const rule = {
|
|
839
1027
|
defaultOptions: [],
|
|
840
1028
|
meta: {
|
|
841
|
-
docs: { description: 'Format Vue templates: strip unnecessary props/$props prefixes, redundant ="true" attributes (except aria- attributes), enforce t() over $t(), require "on" prefix on named event listener handlers,
|
|
1029
|
+
docs: { description: 'Format Vue templates: strip unnecessary props/$props prefixes, redundant ="true" attributes (except aria- attributes), enforce t() over $t(), require "on" prefix on named event listener handlers, disallow inline event handler expressions, require object syntax for dynamic :class bindings, and keep single-property :class objects on one line while spreading multi-property objects across lines' },
|
|
842
1030
|
fixable: "code",
|
|
843
1031
|
messages: {
|
|
1032
|
+
dynamicClassMustBeObject: `Dynamic :class binding must use object syntax (e.g. :class="{ 'foo': condition }"). Ternaries, arrays, strings and bare variables are not allowed.`,
|
|
844
1033
|
eventHandlerMustBeFunction: 'Event listener must reference a named handler function (e.g. @click="onClose") or call one (e.g. @click="onClose($event)"). Inline expressions are not allowed.',
|
|
1034
|
+
multilineClassObject: "Dynamic :class object with multiple properties must span multiple lines, one property per line.",
|
|
845
1035
|
noPropsPrefix: "Unnecessary props/$props prefix in template. Props are automatically available in template scope.",
|
|
846
1036
|
prefixEventHandlerWithOn: 'Event listener handler "{{name}}" must be prefixed with "on" (e.g. @click="onClick").',
|
|
847
1037
|
removeTrueAttribute: 'Unnecessary ="true" attribute. Use the attribute name directly instead.',
|
|
1038
|
+
singleLineClassObject: "Dynamic :class object with a single property must stay on one line.",
|
|
848
1039
|
useT: "Use t() instead of $t() in Vue templates as it does not work with <i18n> tags."
|
|
849
1040
|
},
|
|
850
1041
|
schema: [],
|
|
@@ -957,6 +1148,68 @@ const rule = {
|
|
|
957
1148
|
}
|
|
958
1149
|
eventHandlerMatch = eventHandlerRegex.exec(templateContent);
|
|
959
1150
|
}
|
|
1151
|
+
classBindingRegex.lastIndex = 0;
|
|
1152
|
+
let classMatch = classBindingRegex.exec(templateContent);
|
|
1153
|
+
while (classMatch !== null) {
|
|
1154
|
+
const rawValue = classMatch[1];
|
|
1155
|
+
const trimmedValue = rawValue.trim();
|
|
1156
|
+
if (trimmedValue === "") {
|
|
1157
|
+
classMatch = classBindingRegex.exec(templateContent);
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
const valueStart = templateStartIndex + classMatch.index + classMatch[0].length - 1 - rawValue.length;
|
|
1161
|
+
const lineStart = templateContent.lastIndexOf("\n", classMatch.index) + 1;
|
|
1162
|
+
const baseIndent = (/^[ \t]*/.exec(templateContent.slice(lineStart)) ?? [""])[0];
|
|
1163
|
+
if (trimmedValue.startsWith("{")) {
|
|
1164
|
+
const objectStartInRaw = rawValue.indexOf("{");
|
|
1165
|
+
const objectEndInRaw = rawValue.lastIndexOf("}");
|
|
1166
|
+
if (objectEndInRaw > objectStartInRaw) {
|
|
1167
|
+
const objectText = rawValue.slice(objectStartInRaw, objectEndInRaw + 1);
|
|
1168
|
+
const properties = splitClassObjectProperties(rawValue.slice(objectStartInRaw + 1, objectEndInRaw));
|
|
1169
|
+
const isMultiline = objectText.includes("\n");
|
|
1170
|
+
const shouldBeMultiline = properties.length >= 2;
|
|
1171
|
+
if (properties.length > 0 && shouldBeMultiline !== isMultiline) {
|
|
1172
|
+
const objectStart = valueStart + objectStartInRaw;
|
|
1173
|
+
const objectEnd = valueStart + objectEndInRaw + 1;
|
|
1174
|
+
context.report({
|
|
1175
|
+
fix: (fixer) => fixer.replaceTextRange([objectStart, objectEnd], buildClassObjectText(properties, baseIndent)),
|
|
1176
|
+
loc: {
|
|
1177
|
+
end: sourceCode.getLocFromIndex(objectEnd),
|
|
1178
|
+
start: sourceCode.getLocFromIndex(objectStart)
|
|
1179
|
+
},
|
|
1180
|
+
messageId: shouldBeMultiline ? "multilineClassObject" : "singleLineClassObject"
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
classMatch = classBindingRegex.exec(templateContent);
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
const valueEnd = valueStart + rawValue.length;
|
|
1188
|
+
const ternaryMatch = simpleTernaryRegex.exec(trimmedValue);
|
|
1189
|
+
let fix;
|
|
1190
|
+
if (ternaryMatch) {
|
|
1191
|
+
const condition = ternaryMatch[1].trim();
|
|
1192
|
+
const truthyClass = ternaryMatch[2];
|
|
1193
|
+
const falsyClass = ternaryMatch[3];
|
|
1194
|
+
const entries = [];
|
|
1195
|
+
if (truthyClass !== "") {
|
|
1196
|
+
entries.push(`'${truthyClass}': ${condition}`);
|
|
1197
|
+
}
|
|
1198
|
+
if (falsyClass !== "") {
|
|
1199
|
+
entries.push(`'${falsyClass}': !(${condition})`);
|
|
1200
|
+
}
|
|
1201
|
+
fix = (fixer) => fixer.replaceTextRange([valueStart, valueEnd], buildClassObjectText(entries, baseIndent));
|
|
1202
|
+
}
|
|
1203
|
+
context.report({
|
|
1204
|
+
...fix ? { fix } : {},
|
|
1205
|
+
loc: {
|
|
1206
|
+
end: sourceCode.getLocFromIndex(valueEnd),
|
|
1207
|
+
start: sourceCode.getLocFromIndex(valueStart)
|
|
1208
|
+
},
|
|
1209
|
+
messageId: "dynamicClassMustBeObject"
|
|
1210
|
+
});
|
|
1211
|
+
classMatch = classBindingRegex.exec(templateContent);
|
|
1212
|
+
}
|
|
960
1213
|
}
|
|
961
1214
|
};
|
|
962
1215
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -5198,12 +5198,12 @@ declare const _default: {
|
|
|
5198
5198
|
maxLineLength?: number;
|
|
5199
5199
|
minItems?: number;
|
|
5200
5200
|
} | undefined)?], unknown, RuleListener>;
|
|
5201
|
-
'ts-format-tests': RuleModule<"sortError", [], unknown, RuleListener>;
|
|
5201
|
+
'ts-format-tests': RuleModule<"setupAfterAssertion" | "sortError", [], unknown, RuleListener>;
|
|
5202
5202
|
'vue-format-i18n': RuleModule<"sortError" | "indentError" | "invalidJson" | "invalidLocale" | "missingLocale" | "missingTranslations" | "unusedString", [({
|
|
5203
5203
|
locales?: string[];
|
|
5204
5204
|
} | undefined)?], unknown, RuleListener>;
|
|
5205
5205
|
'vue-format-script': RuleModule<"multilineComputed" | "untypedEmits", [], unknown, RuleListener>;
|
|
5206
|
-
'vue-format-template': RuleModule<"eventHandlerMustBeFunction" | "noPropsPrefix" | "prefixEventHandlerWithOn" | "removeTrueAttribute" | "useT", [], unknown, RuleListener>;
|
|
5206
|
+
'vue-format-template': RuleModule<"dynamicClassMustBeObject" | "eventHandlerMustBeFunction" | "multilineClassObject" | "noPropsPrefix" | "prefixEventHandlerWithOn" | "removeTrueAttribute" | "singleLineClassObject" | "useT", [], unknown, RuleListener>;
|
|
5207
5207
|
};
|
|
5208
5208
|
};
|
|
5209
5209
|
|
package/dist/index.d.mts
CHANGED
|
@@ -5198,12 +5198,12 @@ declare const _default: {
|
|
|
5198
5198
|
maxLineLength?: number;
|
|
5199
5199
|
minItems?: number;
|
|
5200
5200
|
} | undefined)?], unknown, RuleListener>;
|
|
5201
|
-
'ts-format-tests': RuleModule<"sortError", [], unknown, RuleListener>;
|
|
5201
|
+
'ts-format-tests': RuleModule<"setupAfterAssertion" | "sortError", [], unknown, RuleListener>;
|
|
5202
5202
|
'vue-format-i18n': RuleModule<"sortError" | "indentError" | "invalidJson" | "invalidLocale" | "missingLocale" | "missingTranslations" | "unusedString", [({
|
|
5203
5203
|
locales?: string[];
|
|
5204
5204
|
} | undefined)?], unknown, RuleListener>;
|
|
5205
5205
|
'vue-format-script': RuleModule<"multilineComputed" | "untypedEmits", [], unknown, RuleListener>;
|
|
5206
|
-
'vue-format-template': RuleModule<"eventHandlerMustBeFunction" | "noPropsPrefix" | "prefixEventHandlerWithOn" | "removeTrueAttribute" | "useT", [], unknown, RuleListener>;
|
|
5206
|
+
'vue-format-template': RuleModule<"dynamicClassMustBeObject" | "eventHandlerMustBeFunction" | "multilineClassObject" | "noPropsPrefix" | "prefixEventHandlerWithOn" | "removeTrueAttribute" | "singleLineClassObject" | "useT", [], unknown, RuleListener>;
|
|
5207
5207
|
};
|
|
5208
5208
|
};
|
|
5209
5209
|
|
package/dist/index.d.ts
CHANGED
|
@@ -5198,12 +5198,12 @@ declare const _default: {
|
|
|
5198
5198
|
maxLineLength?: number;
|
|
5199
5199
|
minItems?: number;
|
|
5200
5200
|
} | undefined)?], unknown, RuleListener>;
|
|
5201
|
-
'ts-format-tests': RuleModule<"sortError", [], unknown, RuleListener>;
|
|
5201
|
+
'ts-format-tests': RuleModule<"setupAfterAssertion" | "sortError", [], unknown, RuleListener>;
|
|
5202
5202
|
'vue-format-i18n': RuleModule<"sortError" | "indentError" | "invalidJson" | "invalidLocale" | "missingLocale" | "missingTranslations" | "unusedString", [({
|
|
5203
5203
|
locales?: string[];
|
|
5204
5204
|
} | undefined)?], unknown, RuleListener>;
|
|
5205
5205
|
'vue-format-script': RuleModule<"multilineComputed" | "untypedEmits", [], unknown, RuleListener>;
|
|
5206
|
-
'vue-format-template': RuleModule<"eventHandlerMustBeFunction" | "noPropsPrefix" | "prefixEventHandlerWithOn" | "removeTrueAttribute" | "useT", [], unknown, RuleListener>;
|
|
5206
|
+
'vue-format-template': RuleModule<"dynamicClassMustBeObject" | "eventHandlerMustBeFunction" | "multilineClassObject" | "noPropsPrefix" | "prefixEventHandlerWithOn" | "removeTrueAttribute" | "singleLineClassObject" | "useT", [], unknown, RuleListener>;
|
|
5207
5207
|
};
|
|
5208
5208
|
};
|
|
5209
5209
|
|
package/dist/index.mjs
CHANGED
|
@@ -61,7 +61,7 @@ function checkUnion(context, node, maxLineLength, minItems) {
|
|
|
61
61
|
function collapseBlankLine(text) {
|
|
62
62
|
return text.replace(/\n[^\S\n]*\n[^\S\n]*/, "\n");
|
|
63
63
|
}
|
|
64
|
-
function expressionStartsWithIdentifier(node, identifier) {
|
|
64
|
+
function expressionStartsWithIdentifier$1(node, identifier) {
|
|
65
65
|
if (node.type === distExports.AST_NODE_TYPES.Identifier && node.name === identifier) {
|
|
66
66
|
return true;
|
|
67
67
|
}
|
|
@@ -71,17 +71,17 @@ function expressionStartsWithIdentifier(node, identifier) {
|
|
|
71
71
|
return true;
|
|
72
72
|
}
|
|
73
73
|
if (callee.type === distExports.AST_NODE_TYPES.MemberExpression) {
|
|
74
|
-
return expressionStartsWithIdentifier(callee.object, identifier);
|
|
74
|
+
return expressionStartsWithIdentifier$1(callee.object, identifier);
|
|
75
75
|
}
|
|
76
76
|
if (callee.type === distExports.AST_NODE_TYPES.ChainExpression) {
|
|
77
|
-
return expressionStartsWithIdentifier(callee.expression, identifier);
|
|
77
|
+
return expressionStartsWithIdentifier$1(callee.expression, identifier);
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
if (node.type === distExports.AST_NODE_TYPES.MemberExpression) {
|
|
81
|
-
return expressionStartsWithIdentifier(node.object, identifier);
|
|
81
|
+
return expressionStartsWithIdentifier$1(node.object, identifier);
|
|
82
82
|
}
|
|
83
83
|
if (node.type === distExports.AST_NODE_TYPES.ChainExpression) {
|
|
84
|
-
return expressionStartsWithIdentifier(node.expression, identifier);
|
|
84
|
+
return expressionStartsWithIdentifier$1(node.expression, identifier);
|
|
85
85
|
}
|
|
86
86
|
return false;
|
|
87
87
|
}
|
|
@@ -209,10 +209,10 @@ function hasLineCommentBetween(prevNode, nextNode, sourceCode) {
|
|
|
209
209
|
});
|
|
210
210
|
}
|
|
211
211
|
function isAssertExpression(expression) {
|
|
212
|
-
return expressionStartsWithIdentifier(expression, "assert");
|
|
212
|
+
return expressionStartsWithIdentifier$1(expression, "assert");
|
|
213
213
|
}
|
|
214
214
|
function isConsoleExpression(expression) {
|
|
215
|
-
return expressionStartsWithIdentifier(expression, "console");
|
|
215
|
+
return expressionStartsWithIdentifier$1(expression, "console");
|
|
216
216
|
}
|
|
217
217
|
function isDirectivePrologue(node) {
|
|
218
218
|
if (node.expression.type !== distExports.AST_NODE_TYPES.Literal) {
|
|
@@ -221,7 +221,7 @@ function isDirectivePrologue(node) {
|
|
|
221
221
|
return typeof node.expression.value === "string";
|
|
222
222
|
}
|
|
223
223
|
function isExpectExpression(expression) {
|
|
224
|
-
return expressionStartsWithIdentifier(expression, "expect");
|
|
224
|
+
return expressionStartsWithIdentifier$1(expression, "expect");
|
|
225
225
|
}
|
|
226
226
|
function isOwnLineComment(comment, sourceCode) {
|
|
227
227
|
if (comment.type !== "Line") {
|
|
@@ -401,6 +401,7 @@ const rule$4 = {
|
|
|
401
401
|
}
|
|
402
402
|
};
|
|
403
403
|
|
|
404
|
+
const teardownViMethods = /* @__PURE__ */ new Set(["restoreAllMocks", "unstubAllGlobals", "useRealTimers"]);
|
|
404
405
|
function compareTests(testA, testB) {
|
|
405
406
|
const functionA = getFunctionName(testA);
|
|
406
407
|
const functionB = getFunctionName(testB);
|
|
@@ -416,6 +417,40 @@ function compareTests(testA, testB) {
|
|
|
416
417
|
const specificNameB = getSpecificName(testB);
|
|
417
418
|
return specificNameA.localeCompare(specificNameB);
|
|
418
419
|
}
|
|
420
|
+
function expressionStartsWithIdentifier(node, identifier) {
|
|
421
|
+
if (node.type === distExports.AST_NODE_TYPES.Identifier && node.name === identifier) {
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
if (node.type === distExports.AST_NODE_TYPES.CallExpression) {
|
|
425
|
+
const { callee } = node;
|
|
426
|
+
if (callee.type === distExports.AST_NODE_TYPES.Identifier && callee.name === identifier) {
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
if (callee.type === distExports.AST_NODE_TYPES.MemberExpression) {
|
|
430
|
+
return expressionStartsWithIdentifier(callee.object, identifier);
|
|
431
|
+
}
|
|
432
|
+
if (callee.type === distExports.AST_NODE_TYPES.ChainExpression) {
|
|
433
|
+
return expressionStartsWithIdentifier(callee.expression, identifier);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (node.type === distExports.AST_NODE_TYPES.MemberExpression) {
|
|
437
|
+
return expressionStartsWithIdentifier(node.object, identifier);
|
|
438
|
+
}
|
|
439
|
+
if (node.type === distExports.AST_NODE_TYPES.ChainExpression) {
|
|
440
|
+
return expressionStartsWithIdentifier(node.expression, identifier);
|
|
441
|
+
}
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
function getExpressionFromStatement(statement) {
|
|
445
|
+
if (statement.type !== distExports.AST_NODE_TYPES.ExpressionStatement) {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
let { expression } = statement;
|
|
449
|
+
if (expression.type === distExports.AST_NODE_TYPES.AwaitExpression) {
|
|
450
|
+
expression = expression.argument;
|
|
451
|
+
}
|
|
452
|
+
return expression;
|
|
453
|
+
}
|
|
419
454
|
function getFunctionName(testName) {
|
|
420
455
|
const parts = testName.split(".");
|
|
421
456
|
return parts.slice(0, 2).join(".");
|
|
@@ -429,6 +464,20 @@ function getSpecificName(testName) {
|
|
|
429
464
|
}
|
|
430
465
|
return parts.slice(specialIndex + 1).join(".");
|
|
431
466
|
}
|
|
467
|
+
function getTestCallbackBlockBody(node) {
|
|
468
|
+
const callback = node.arguments[1];
|
|
469
|
+
if (!callback) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
if (callback.type !== distExports.AST_NODE_TYPES.FunctionExpression && callback.type !== distExports.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
const { body } = callback;
|
|
476
|
+
if (body.type !== distExports.AST_NODE_TYPES.BlockStatement) {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
return body;
|
|
480
|
+
}
|
|
432
481
|
function getTestName(node) {
|
|
433
482
|
const firstArgument = node.arguments[0];
|
|
434
483
|
if (firstArgument?.type === distExports.AST_NODE_TYPES.Literal && typeof firstArgument.value === "string") {
|
|
@@ -449,12 +498,78 @@ function getTestPriority(testName) {
|
|
|
449
498
|
return 1;
|
|
450
499
|
}
|
|
451
500
|
}
|
|
501
|
+
function hasAssertMethodCall(expression) {
|
|
502
|
+
if (expression.type !== distExports.AST_NODE_TYPES.CallExpression) {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
const { callee } = expression;
|
|
506
|
+
if (callee.type === distExports.AST_NODE_TYPES.MemberExpression && callee.property.type === distExports.AST_NODE_TYPES.Identifier && callee.property.name.startsWith("assert")) {
|
|
507
|
+
if (callee.object.type === distExports.AST_NODE_TYPES.Identifier && callee.object.name === "expect") {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
function isAssertionCountExpression(expression) {
|
|
515
|
+
if (expression.type !== distExports.AST_NODE_TYPES.CallExpression) {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
const { callee } = expression;
|
|
519
|
+
if (callee.type !== distExports.AST_NODE_TYPES.MemberExpression || callee.object.type !== distExports.AST_NODE_TYPES.Identifier || callee.property.type !== distExports.AST_NODE_TYPES.Identifier) {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
if (callee.object.name === "expect" && (callee.property.name === "assertions" || callee.property.name === "hasAssertions")) {
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
return callee.object.name === "assert" && callee.property.name === "plan";
|
|
526
|
+
}
|
|
527
|
+
function isAssertionCountStatement(statement) {
|
|
528
|
+
const expression = getExpressionFromStatement(statement);
|
|
529
|
+
if (!expression) {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
return isAssertionCountExpression(expression);
|
|
533
|
+
}
|
|
534
|
+
function isAssertionExpression(expression) {
|
|
535
|
+
if (isAssertionCountExpression(expression)) {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
if (expressionStartsWithIdentifier(expression, "assert") || expressionStartsWithIdentifier(expression, "expect")) {
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
return hasAssertMethodCall(expression);
|
|
542
|
+
}
|
|
543
|
+
function isAssertionStatement(statement) {
|
|
544
|
+
const expression = getExpressionFromStatement(statement);
|
|
545
|
+
if (!expression) {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
return isAssertionExpression(expression);
|
|
549
|
+
}
|
|
550
|
+
function isTeardownStatement(statement) {
|
|
551
|
+
const expression = getExpressionFromStatement(statement);
|
|
552
|
+
if (!expression || expression.type !== distExports.AST_NODE_TYPES.CallExpression) {
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
const { callee } = expression;
|
|
556
|
+
if (callee.type === distExports.AST_NODE_TYPES.Identifier && callee.name === "cleanup") {
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
if (callee.type === distExports.AST_NODE_TYPES.MemberExpression && callee.object.type === distExports.AST_NODE_TYPES.Identifier && callee.object.name === "vi" && callee.property.type === distExports.AST_NODE_TYPES.Identifier && teardownViMethods.has(callee.property.name)) {
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
452
564
|
const rule$3 = {
|
|
453
565
|
defaultOptions: [],
|
|
454
566
|
meta: {
|
|
455
|
-
docs: { description: "Enforce sorted test functions
|
|
567
|
+
docs: { description: "Enforce sorted test functions and setup-before-assertions layout in test bodies" },
|
|
456
568
|
fixable: "code",
|
|
457
|
-
messages: {
|
|
569
|
+
messages: {
|
|
570
|
+
setupAfterAssertion: "Setup statements must appear before assertions in test bodies.",
|
|
571
|
+
sortError: "Test functions should be grouped by method with sorted metrics, errors, exceptions and middlewares."
|
|
572
|
+
},
|
|
458
573
|
schema: [],
|
|
459
574
|
type: "suggestion"
|
|
460
575
|
},
|
|
@@ -462,6 +577,25 @@ const rule$3 = {
|
|
|
462
577
|
if (!context.filename.endsWith(".spec.ts") && !context.filename.endsWith(".test.ts")) {
|
|
463
578
|
return {};
|
|
464
579
|
}
|
|
580
|
+
function handleTestBody(node) {
|
|
581
|
+
const callbackBody = getTestCallbackBlockBody(node);
|
|
582
|
+
if (!callbackBody) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const firstAssertionIndex = callbackBody.body.findIndex(isAssertionStatement);
|
|
586
|
+
if (firstAssertionIndex === -1) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
for (const statement of callbackBody.body.slice(firstAssertionIndex + 1)) {
|
|
590
|
+
if (isAssertionStatement(statement) || isAssertionCountStatement(statement) || isTeardownStatement(statement)) {
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
context.report({
|
|
594
|
+
messageId: "setupAfterAssertion",
|
|
595
|
+
node: statement
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
465
599
|
function handleTestGroup(node) {
|
|
466
600
|
const testGroup = node.arguments[0];
|
|
467
601
|
if (!testGroup || !node.arguments[1]) {
|
|
@@ -510,6 +644,8 @@ const rule$3 = {
|
|
|
510
644
|
}
|
|
511
645
|
return {
|
|
512
646
|
'CallExpression[callee.name="describe"]': handleTestGroup,
|
|
647
|
+
'CallExpression[callee.name="it"]': handleTestBody,
|
|
648
|
+
'CallExpression[callee.name="test"]': handleTestBody,
|
|
513
649
|
'CallExpression[callee.object.name="describe"][callee.property.name="concurrent"]': handleTestGroup,
|
|
514
650
|
'CallExpression[callee.object.name="test"][callee.property.name="group"]': handleTestGroup
|
|
515
651
|
};
|
|
@@ -808,9 +944,61 @@ const propsPrefixRegex = /\$?props\.(\w+)/g;
|
|
|
808
944
|
const trueAttributeRegex = /(?<![\w-]):?(?!aria-)([a-z0-9-]+)="true"/gi;
|
|
809
945
|
const eventHandlerRegex = /(?:@|v-on:)[^\s="'<>/]+\s*=\s*"([^"]*)"/g;
|
|
810
946
|
const bareIdentifierRegex = /^[a-z_$][\w$]*$/i;
|
|
947
|
+
const classBindingRegex = /(?<![\w-])(?::|v-bind:)class\s*=\s*"([^"]*)"/g;
|
|
948
|
+
const simpleTernaryRegex = /^([^?]+)\?\s*'([^']*)'\s*:\s*'([^']*)'$/;
|
|
811
949
|
const singleCallRegex = /^([a-z_$][\w$]*)\s*\(.*\)$/is;
|
|
812
950
|
const onPrefixRegex = /^on[A-Z]/;
|
|
813
951
|
const exemptCallees = /* @__PURE__ */ new Set(["$emit", "emit"]);
|
|
952
|
+
function splitClassObjectProperties(body) {
|
|
953
|
+
const properties = [];
|
|
954
|
+
let depth = 0;
|
|
955
|
+
let quote;
|
|
956
|
+
let current = "";
|
|
957
|
+
for (let index = 0; index < body.length; index++) {
|
|
958
|
+
const char = body[index];
|
|
959
|
+
if (quote !== void 0) {
|
|
960
|
+
current += char;
|
|
961
|
+
if (char === quote && body[index - 1] !== "\\") {
|
|
962
|
+
quote = void 0;
|
|
963
|
+
}
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
967
|
+
quote = char;
|
|
968
|
+
current += char;
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
if (char === "(" || char === "[" || char === "{") {
|
|
972
|
+
depth++;
|
|
973
|
+
} else if (char === ")" || char === "]" || char === "}") {
|
|
974
|
+
depth--;
|
|
975
|
+
}
|
|
976
|
+
if (char === "," && depth === 0) {
|
|
977
|
+
const property2 = current.trim();
|
|
978
|
+
if (property2 !== "") {
|
|
979
|
+
properties.push(property2);
|
|
980
|
+
}
|
|
981
|
+
current = "";
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
current += char;
|
|
985
|
+
}
|
|
986
|
+
const property = current.trim();
|
|
987
|
+
if (property !== "") {
|
|
988
|
+
properties.push(property);
|
|
989
|
+
}
|
|
990
|
+
return properties;
|
|
991
|
+
}
|
|
992
|
+
function buildClassObjectText(properties, baseIndent) {
|
|
993
|
+
if (properties.length <= 1) {
|
|
994
|
+
return `{ ${properties.join(", ")} }`;
|
|
995
|
+
}
|
|
996
|
+
const propertyIndent = `${baseIndent} `;
|
|
997
|
+
const lines = properties.map((property) => `${propertyIndent}${property},`);
|
|
998
|
+
return `{
|
|
999
|
+
${lines.join("\n")}
|
|
1000
|
+
${baseIndent}}`;
|
|
1001
|
+
}
|
|
814
1002
|
const dollarTPatterns = [
|
|
815
1003
|
{
|
|
816
1004
|
pattern: / \$t\(/g,
|
|
@@ -836,13 +1024,16 @@ const dollarTPatterns = [
|
|
|
836
1024
|
const rule = {
|
|
837
1025
|
defaultOptions: [],
|
|
838
1026
|
meta: {
|
|
839
|
-
docs: { description: 'Format Vue templates: strip unnecessary props/$props prefixes, redundant ="true" attributes (except aria- attributes), enforce t() over $t(), require "on" prefix on named event listener handlers,
|
|
1027
|
+
docs: { description: 'Format Vue templates: strip unnecessary props/$props prefixes, redundant ="true" attributes (except aria- attributes), enforce t() over $t(), require "on" prefix on named event listener handlers, disallow inline event handler expressions, require object syntax for dynamic :class bindings, and keep single-property :class objects on one line while spreading multi-property objects across lines' },
|
|
840
1028
|
fixable: "code",
|
|
841
1029
|
messages: {
|
|
1030
|
+
dynamicClassMustBeObject: `Dynamic :class binding must use object syntax (e.g. :class="{ 'foo': condition }"). Ternaries, arrays, strings and bare variables are not allowed.`,
|
|
842
1031
|
eventHandlerMustBeFunction: 'Event listener must reference a named handler function (e.g. @click="onClose") or call one (e.g. @click="onClose($event)"). Inline expressions are not allowed.',
|
|
1032
|
+
multilineClassObject: "Dynamic :class object with multiple properties must span multiple lines, one property per line.",
|
|
843
1033
|
noPropsPrefix: "Unnecessary props/$props prefix in template. Props are automatically available in template scope.",
|
|
844
1034
|
prefixEventHandlerWithOn: 'Event listener handler "{{name}}" must be prefixed with "on" (e.g. @click="onClick").',
|
|
845
1035
|
removeTrueAttribute: 'Unnecessary ="true" attribute. Use the attribute name directly instead.',
|
|
1036
|
+
singleLineClassObject: "Dynamic :class object with a single property must stay on one line.",
|
|
846
1037
|
useT: "Use t() instead of $t() in Vue templates as it does not work with <i18n> tags."
|
|
847
1038
|
},
|
|
848
1039
|
schema: [],
|
|
@@ -955,6 +1146,68 @@ const rule = {
|
|
|
955
1146
|
}
|
|
956
1147
|
eventHandlerMatch = eventHandlerRegex.exec(templateContent);
|
|
957
1148
|
}
|
|
1149
|
+
classBindingRegex.lastIndex = 0;
|
|
1150
|
+
let classMatch = classBindingRegex.exec(templateContent);
|
|
1151
|
+
while (classMatch !== null) {
|
|
1152
|
+
const rawValue = classMatch[1];
|
|
1153
|
+
const trimmedValue = rawValue.trim();
|
|
1154
|
+
if (trimmedValue === "") {
|
|
1155
|
+
classMatch = classBindingRegex.exec(templateContent);
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
const valueStart = templateStartIndex + classMatch.index + classMatch[0].length - 1 - rawValue.length;
|
|
1159
|
+
const lineStart = templateContent.lastIndexOf("\n", classMatch.index) + 1;
|
|
1160
|
+
const baseIndent = (/^[ \t]*/.exec(templateContent.slice(lineStart)) ?? [""])[0];
|
|
1161
|
+
if (trimmedValue.startsWith("{")) {
|
|
1162
|
+
const objectStartInRaw = rawValue.indexOf("{");
|
|
1163
|
+
const objectEndInRaw = rawValue.lastIndexOf("}");
|
|
1164
|
+
if (objectEndInRaw > objectStartInRaw) {
|
|
1165
|
+
const objectText = rawValue.slice(objectStartInRaw, objectEndInRaw + 1);
|
|
1166
|
+
const properties = splitClassObjectProperties(rawValue.slice(objectStartInRaw + 1, objectEndInRaw));
|
|
1167
|
+
const isMultiline = objectText.includes("\n");
|
|
1168
|
+
const shouldBeMultiline = properties.length >= 2;
|
|
1169
|
+
if (properties.length > 0 && shouldBeMultiline !== isMultiline) {
|
|
1170
|
+
const objectStart = valueStart + objectStartInRaw;
|
|
1171
|
+
const objectEnd = valueStart + objectEndInRaw + 1;
|
|
1172
|
+
context.report({
|
|
1173
|
+
fix: (fixer) => fixer.replaceTextRange([objectStart, objectEnd], buildClassObjectText(properties, baseIndent)),
|
|
1174
|
+
loc: {
|
|
1175
|
+
end: sourceCode.getLocFromIndex(objectEnd),
|
|
1176
|
+
start: sourceCode.getLocFromIndex(objectStart)
|
|
1177
|
+
},
|
|
1178
|
+
messageId: shouldBeMultiline ? "multilineClassObject" : "singleLineClassObject"
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
classMatch = classBindingRegex.exec(templateContent);
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
const valueEnd = valueStart + rawValue.length;
|
|
1186
|
+
const ternaryMatch = simpleTernaryRegex.exec(trimmedValue);
|
|
1187
|
+
let fix;
|
|
1188
|
+
if (ternaryMatch) {
|
|
1189
|
+
const condition = ternaryMatch[1].trim();
|
|
1190
|
+
const truthyClass = ternaryMatch[2];
|
|
1191
|
+
const falsyClass = ternaryMatch[3];
|
|
1192
|
+
const entries = [];
|
|
1193
|
+
if (truthyClass !== "") {
|
|
1194
|
+
entries.push(`'${truthyClass}': ${condition}`);
|
|
1195
|
+
}
|
|
1196
|
+
if (falsyClass !== "") {
|
|
1197
|
+
entries.push(`'${falsyClass}': !(${condition})`);
|
|
1198
|
+
}
|
|
1199
|
+
fix = (fixer) => fixer.replaceTextRange([valueStart, valueEnd], buildClassObjectText(entries, baseIndent));
|
|
1200
|
+
}
|
|
1201
|
+
context.report({
|
|
1202
|
+
...fix ? { fix } : {},
|
|
1203
|
+
loc: {
|
|
1204
|
+
end: sourceCode.getLocFromIndex(valueEnd),
|
|
1205
|
+
start: sourceCode.getLocFromIndex(valueStart)
|
|
1206
|
+
},
|
|
1207
|
+
messageId: "dynamicClassMustBeObject"
|
|
1208
|
+
});
|
|
1209
|
+
classMatch = classBindingRegex.exec(templateContent);
|
|
1210
|
+
}
|
|
958
1211
|
}
|
|
959
1212
|
};
|
|
960
1213
|
}
|