eslint-plugin-flawless 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -211,6 +211,7 @@ Requires [type information](https://typescript-eslint.io/linting/typed-linting).
211
211
  | [no-unnecessary-use-memo](src/rules/no-unnecessary-use-memo/documentation.md) | Disallow unnecessary usage of 'useMemo' | | |
212
212
  | [padding-after-expect-assertions](src/rules/padding-after-expect-assertions/documentation.md) | Enforce a blank line after `expect.assertions` and `expect.hasAssertions` | 🔧 | |
213
213
  | [prefer-destructuring-assignment](src/rules/prefer-destructuring-assignment/documentation.md) | Enforce destructuring assignment for component props | 🔧 | |
214
+ | [prefer-ending-with-an-expect](src/rules/prefer-ending-with-an-expect/documentation.md) | Prefer having the last statement in a test be an assertion | | |
214
215
  | [prefer-parameter-destructuring](src/rules/prefer-parameter-destructuring/documentation.md) | Enforce destructuring parameters in the function signature | 🔧 | |
215
216
  | [prefer-read-only-props](src/rules/prefer-read-only-props/documentation.md) | Enforce that function component props are read-only | 🔧 | 💭 |
216
217
  | [purity](src/rules/purity/documentation.md) | Disallow impure calls such as `math.random` or `os.clock` during render | | |
package/dist/index.d.mts CHANGED
@@ -51,7 +51,7 @@ declare const EXPLICIT = "useExplicitReturn";
51
51
  declare const COMPLEX_EXPLICIT = "useExplicitReturnComplex";
52
52
  type MessageIds$5 = typeof COMPLEX_EXPLICIT | typeof EXPLICIT | typeof IMPLICIT;
53
53
  type ObjectReturnStyle = "always-explicit" | "complex-explicit" | "off";
54
- type Options$6 = [{
54
+ type Options$7 = [{
55
55
  /** Always use explicit returns for JSX bodies. */
56
56
  jsxAlwaysUseExplicitReturn?: boolean;
57
57
  /** Maximum emitted line length (tab-expanded) before requiring an explicit return. */
@@ -86,7 +86,7 @@ interface JsxShorthandFragmentOptions {
86
86
  */
87
87
  readonly mode?: Mode;
88
88
  }
89
- type Options$5 = [JsxShorthandFragmentOptions?];
89
+ type Options$6 = [JsxShorthandFragmentOptions?];
90
90
  type Mode = "element" | "syntax";
91
91
  //#endregion
92
92
  //#region src/rules/max-lines-per-function/rule.d.ts
@@ -119,7 +119,7 @@ interface MaxLinesPerFunctionOptions {
119
119
  */
120
120
  readonly skipComments?: boolean;
121
121
  }
122
- type Options$4 = [MaxLinesPerFunctionOptions?];
122
+ type Options$5 = [MaxLinesPerFunctionOptions?];
123
123
  //#endregion
124
124
  //#region src/rules/naming-convention/utils/enums.d.ts
125
125
  declare const Selector: {
@@ -246,7 +246,7 @@ interface NamingSelector {
246
246
  //#endregion
247
247
  //#region src/rules/naming-convention/rule.d.ts
248
248
  type MessageIds$3 = "doesNotMatchFormat" | "doesNotMatchFormatForeignContract" | "doesNotMatchFormatTrimmed" | "doesNotMatchFormatTrimmedForeignContract" | "missingAffix" | "missingAffixForeignContract" | "missingUnderscore" | "missingUnderscoreForeignContract" | "satisfyCustom" | "satisfyCustomForeignContract" | "unexpectedUnderscore" | "unexpectedUnderscoreForeignContract";
249
- type Options$3 = Array<NamingSelector>;
249
+ type Options$4 = Array<NamingSelector>;
250
250
  //#endregion
251
251
  //#region src/rules/no-unnecessary-use-callback/rule.d.ts
252
252
  declare const MESSAGE_ID_DEFAULT$1 = "default";
@@ -258,6 +258,24 @@ declare const MESSAGE_ID_DEFAULT = "default";
258
258
  declare const MESSAGE_ID_INSIDE_USE_EFFECT = "noUnnecessaryUseMemoInsideUseEffect";
259
259
  type MessageIds$1 = typeof MESSAGE_ID_DEFAULT | typeof MESSAGE_ID_INSIDE_USE_EFFECT;
260
260
  //#endregion
261
+ //#region src/rules/prefer-ending-with-an-expect/rule.d.ts
262
+ interface PreferEndingWithAnExpectOptions {
263
+ /**
264
+ * Callee names, in addition to a resolved vitest `it`/`test`, whose second
265
+ * argument is treated as a test body. Matched by exact dotted name (no scope
266
+ * resolution), for libraries with custom test blocks such as `each.test`.
267
+ */
268
+ readonly additionalTestBlockFunctions?: ReadonlyArray<string>;
269
+ /**
270
+ * Function names, in addition to a resolved vitest `expect`, that count as an
271
+ * assertion when they end a test. Matched by name against the callee chain,
272
+ * so no scope resolution applies. A `*` matches a single dotted segment and
273
+ * `**` matches any number, allowing patterns such as `request.*.expect`.
274
+ */
275
+ readonly assertFunctionNames?: ReadonlyArray<string>;
276
+ }
277
+ type Options$3 = [PreferEndingWithAnExpectOptions?];
278
+ //#endregion
261
279
  //#region src/rules/prefer-parameter-destructuring/rule.d.ts
262
280
  type Options$2 = [{
263
281
  /**
@@ -305,11 +323,11 @@ declare const plugin: {
305
323
  version: string;
306
324
  };
307
325
  rules: {
308
- "arrow-return-style": FlawlessRuleModule<Options$6, MessageIds$5, Readonly<TSESLint.SourceCode>>;
326
+ "arrow-return-style": FlawlessRuleModule<Options$7, MessageIds$5, Readonly<TSESLint.SourceCode>>;
309
327
  "jsx-shorthand-boolean": FlawlessRuleModule<[], "setAttributeValue", Readonly<TSESLint.SourceCode>>;
310
- "jsx-shorthand-fragment": FlawlessRuleModule<Options$5, MessageIds$4, Readonly<TSESLint.SourceCode>>;
311
- "max-lines-per-function": FlawlessRuleModule<Options$4, "exceed", Readonly<TSESLint.SourceCode>>;
312
- "naming-convention": TSESLint.RuleModule<MessageIds$3, Options$3, PluginDocumentation, TSESLint.RuleListener> & {
328
+ "jsx-shorthand-fragment": FlawlessRuleModule<Options$6, MessageIds$4, Readonly<TSESLint.SourceCode>>;
329
+ "max-lines-per-function": FlawlessRuleModule<Options$5, "exceed", Readonly<TSESLint.SourceCode>>;
330
+ "naming-convention": TSESLint.RuleModule<MessageIds$3, Options$4, PluginDocumentation, TSESLint.RuleListener> & {
313
331
  name: string;
314
332
  };
315
333
  "no-export-default-arrow": FlawlessRuleModule<[], "disallowExportDefaultArrow", Readonly<TSESLint.SourceCode>>;
@@ -320,6 +338,7 @@ declare const plugin: {
320
338
  "no-unnecessary-use-memo": FlawlessRuleModule<[], MessageIds$1, Readonly<TSESLint.SourceCode>>;
321
339
  "padding-after-expect-assertions": FlawlessRuleModule<[], "missingPadding", Readonly<TSESLint.SourceCode>>;
322
340
  "prefer-destructuring-assignment": FlawlessRuleModule<[], "default", Readonly<TSESLint.SourceCode>>;
341
+ "prefer-ending-with-an-expect": FlawlessRuleModule<Options$3, "mustEndWithExpect", Readonly<TSESLint.SourceCode>>;
323
342
  "prefer-parameter-destructuring": FlawlessRuleModule<Options$2, "default", Readonly<TSESLint.SourceCode>>;
324
343
  "prefer-read-only-props": TSESLint.RuleModule<"preferReadOnlyProps", [{
325
344
  fixStyle?: "modifier" | "wrap";
@@ -350,11 +369,11 @@ declare const _default: {
350
369
  version: string;
351
370
  };
352
371
  rules: {
353
- "arrow-return-style": FlawlessRuleModule<Options$6, MessageIds$5, Readonly<import("${configDir}").SourceCode>>;
372
+ "arrow-return-style": FlawlessRuleModule<Options$7, MessageIds$5, Readonly<import("${configDir}").SourceCode>>;
354
373
  "jsx-shorthand-boolean": FlawlessRuleModule<[], "setAttributeValue", Readonly<import("${configDir}").SourceCode>>;
355
- "jsx-shorthand-fragment": FlawlessRuleModule<Options$5, MessageIds$4, Readonly<import("${configDir}").SourceCode>>;
356
- "max-lines-per-function": FlawlessRuleModule<Options$4, "exceed", Readonly<import("${configDir}").SourceCode>>;
357
- "naming-convention": import("${configDir}").RuleModule<MessageIds$3, Options$3, PluginDocumentation, import("${configDir}").RuleListener> & {
374
+ "jsx-shorthand-fragment": FlawlessRuleModule<Options$6, MessageIds$4, Readonly<import("${configDir}").SourceCode>>;
375
+ "max-lines-per-function": FlawlessRuleModule<Options$5, "exceed", Readonly<import("${configDir}").SourceCode>>;
376
+ "naming-convention": import("${configDir}").RuleModule<MessageIds$3, Options$4, PluginDocumentation, import("${configDir}").RuleListener> & {
358
377
  name: string;
359
378
  };
360
379
  "no-export-default-arrow": FlawlessRuleModule<[], "disallowExportDefaultArrow", Readonly<import("${configDir}").SourceCode>>;
@@ -365,6 +384,7 @@ declare const _default: {
365
384
  "no-unnecessary-use-memo": FlawlessRuleModule<[], MessageIds$1, Readonly<import("${configDir}").SourceCode>>;
366
385
  "padding-after-expect-assertions": FlawlessRuleModule<[], "missingPadding", Readonly<import("${configDir}").SourceCode>>;
367
386
  "prefer-destructuring-assignment": FlawlessRuleModule<[], "default", Readonly<import("${configDir}").SourceCode>>;
387
+ "prefer-ending-with-an-expect": FlawlessRuleModule<Options$3, "mustEndWithExpect", Readonly<import("${configDir}").SourceCode>>;
368
388
  "prefer-parameter-destructuring": FlawlessRuleModule<Options$2, "default", Readonly<import("${configDir}").SourceCode>>;
369
389
  "prefer-read-only-props": import("${configDir}").RuleModule<"preferReadOnlyProps", [{
370
390
  fixStyle?: "modifier" | "wrap";
@@ -392,11 +412,11 @@ declare const _default: {
392
412
  version: string;
393
413
  };
394
414
  rules: {
395
- "arrow-return-style": FlawlessRuleModule<Options$6, MessageIds$5, Readonly<import("${configDir}").SourceCode>>;
415
+ "arrow-return-style": FlawlessRuleModule<Options$7, MessageIds$5, Readonly<import("${configDir}").SourceCode>>;
396
416
  "jsx-shorthand-boolean": FlawlessRuleModule<[], "setAttributeValue", Readonly<import("${configDir}").SourceCode>>;
397
- "jsx-shorthand-fragment": FlawlessRuleModule<Options$5, MessageIds$4, Readonly<import("${configDir}").SourceCode>>;
398
- "max-lines-per-function": FlawlessRuleModule<Options$4, "exceed", Readonly<import("${configDir}").SourceCode>>;
399
- "naming-convention": import("${configDir}").RuleModule<MessageIds$3, Options$3, PluginDocumentation, import("${configDir}").RuleListener> & {
417
+ "jsx-shorthand-fragment": FlawlessRuleModule<Options$6, MessageIds$4, Readonly<import("${configDir}").SourceCode>>;
418
+ "max-lines-per-function": FlawlessRuleModule<Options$5, "exceed", Readonly<import("${configDir}").SourceCode>>;
419
+ "naming-convention": import("${configDir}").RuleModule<MessageIds$3, Options$4, PluginDocumentation, import("${configDir}").RuleListener> & {
400
420
  name: string;
401
421
  };
402
422
  "no-export-default-arrow": FlawlessRuleModule<[], "disallowExportDefaultArrow", Readonly<import("${configDir}").SourceCode>>;
@@ -407,6 +427,7 @@ declare const _default: {
407
427
  "no-unnecessary-use-memo": FlawlessRuleModule<[], MessageIds$1, Readonly<import("${configDir}").SourceCode>>;
408
428
  "padding-after-expect-assertions": FlawlessRuleModule<[], "missingPadding", Readonly<import("${configDir}").SourceCode>>;
409
429
  "prefer-destructuring-assignment": FlawlessRuleModule<[], "default", Readonly<import("${configDir}").SourceCode>>;
430
+ "prefer-ending-with-an-expect": FlawlessRuleModule<Options$3, "mustEndWithExpect", Readonly<import("${configDir}").SourceCode>>;
410
431
  "prefer-parameter-destructuring": FlawlessRuleModule<Options$2, "default", Readonly<import("${configDir}").SourceCode>>;
411
432
  "prefer-read-only-props": import("${configDir}").RuleModule<"preferReadOnlyProps", [{
412
433
  fixStyle?: "modifier" | "wrap";
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as plugin, t as PLUGIN_NAME } from "./plugin-K2GOCG3Z.mjs";
1
+ import { n as plugin, t as PLUGIN_NAME } from "./plugin-CDVxMgSG.mjs";
2
2
  //#region src/configs/index.ts
3
3
  const configs = { recommended: {
4
4
  plugins: { [PLUGIN_NAME]: plugin },
package/dist/oxlint.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as plugin, t as PLUGIN_NAME } from "./plugin-K2GOCG3Z.mjs";
1
+ import { n as plugin, t as PLUGIN_NAME } from "./plugin-CDVxMgSG.mjs";
2
2
  import { definePlugin } from "@oxlint/plugins";
3
3
  //#region src/oxlint.ts
4
4
  /**
@@ -14,7 +14,7 @@ import * as core from "@eslint-react/core";
14
14
  import { getStaticTOMLValue } from "toml-eslint-parser";
15
15
  //#region package.json
16
16
  var name = "eslint-plugin-flawless";
17
- var version = "1.2.0";
17
+ var version = "1.3.0";
18
18
  var repository = {
19
19
  "url": "git+https://github.com/christopher-buss/eslint-plugin-flawless.git",
20
20
  "type": "git"
@@ -84,11 +84,11 @@ function createFlawlessRule({ createOnce, ...meta }) {
84
84
  }
85
85
  //#endregion
86
86
  //#region src/rules/arrow-return-style/rule.ts
87
- const RULE_NAME$16 = "arrow-return-style";
87
+ const RULE_NAME$17 = "arrow-return-style";
88
88
  const IMPLICIT = "useImplicitReturn";
89
89
  const EXPLICIT = "useExplicitReturn";
90
90
  const COMPLEX_EXPLICIT = "useExplicitReturnComplex";
91
- const DEFAULTS$1 = {
91
+ const DEFAULTS$2 = {
92
92
  jsxAlwaysUseExplicitReturn: false,
93
93
  maxLen: 80,
94
94
  maxObjectProperties: 4,
@@ -232,9 +232,9 @@ function collectArrows(root) {
232
232
  return arrows.sort((a, b) => a.range[0] - b.range[0]);
233
233
  }
234
234
  const arrowReturnStyle = createFlawlessRule({
235
- name: RULE_NAME$16,
235
+ name: RULE_NAME$17,
236
236
  createOnce(context) {
237
- let config = DEFAULTS$1;
237
+ let config = DEFAULTS$2;
238
238
  let sourceCode;
239
239
  let lines;
240
240
  let pendingConsults;
@@ -533,7 +533,7 @@ const arrowReturnStyle = createFlawlessRule({
533
533
  },
534
534
  "before": function() {
535
535
  config = {
536
- ...DEFAULTS$1,
536
+ ...DEFAULTS$2,
537
537
  ...context.options[0]
538
538
  };
539
539
  ({sourceCode} = context);
@@ -545,7 +545,7 @@ const arrowReturnStyle = createFlawlessRule({
545
545
  }
546
546
  };
547
547
  },
548
- defaultOptions: [DEFAULTS$1],
548
+ defaultOptions: [DEFAULTS$2],
549
549
  meta: {
550
550
  docs: {
551
551
  description: "Enforce arrow function return style based on line length",
@@ -598,23 +598,23 @@ const arrowReturnStyle = createFlawlessRule({
598
598
  });
599
599
  //#endregion
600
600
  //#region src/rules/jsx-shorthand-boolean/rule.ts
601
- const RULE_NAME$15 = "jsx-shorthand-boolean";
602
- const MESSAGE_ID$7 = "setAttributeValue";
603
- const messages$15 = { [MESSAGE_ID$7]: "Set an explicit value for boolean attribute '{{name}}'." };
604
- function createOnce$10(context) {
601
+ const RULE_NAME$16 = "jsx-shorthand-boolean";
602
+ const MESSAGE_ID$8 = "setAttributeValue";
603
+ const messages$16 = { [MESSAGE_ID$8]: "Set an explicit value for boolean attribute '{{name}}'." };
604
+ function createOnce$11(context) {
605
605
  return { JSXAttribute(node) {
606
606
  if (node.value !== null) return;
607
607
  context.report({
608
608
  data: { name: context.sourceCode.getText(node.name) },
609
609
  fix: (fixer) => fixer.insertTextAfter(node.name, "={true}"),
610
- messageId: MESSAGE_ID$7,
610
+ messageId: MESSAGE_ID$8,
611
611
  node
612
612
  });
613
613
  } };
614
614
  }
615
615
  const jsxShorthandBoolean = createFlawlessRule({
616
- name: RULE_NAME$15,
617
- createOnce: createOnce$10,
616
+ name: RULE_NAME$16,
617
+ createOnce: createOnce$11,
618
618
  defaultOptions: [],
619
619
  meta: {
620
620
  docs: {
@@ -624,23 +624,23 @@ const jsxShorthandBoolean = createFlawlessRule({
624
624
  },
625
625
  fixable: "code",
626
626
  hasSuggestions: false,
627
- messages: messages$15,
627
+ messages: messages$16,
628
628
  schema: [],
629
629
  type: "suggestion"
630
630
  }
631
631
  });
632
632
  //#endregion
633
633
  //#region src/rules/jsx-shorthand-fragment/rule.ts
634
- const RULE_NAME$14 = "jsx-shorthand-fragment";
634
+ const RULE_NAME$15 = "jsx-shorthand-fragment";
635
635
  const MESSAGE_ID_NAMED = "useNamedFragment";
636
636
  const MESSAGE_ID_SHORTHAND = "useShorthandFragment";
637
637
  const DEFAULT_MODE = "syntax";
638
638
  const DEFAULT_FRAGMENT_NAME = "Fragment";
639
- const messages$14 = {
639
+ const messages$15 = {
640
640
  [MESSAGE_ID_NAMED]: "Use the '{{name}}' component instead of fragment shorthand syntax.",
641
641
  [MESSAGE_ID_SHORTHAND]: "Use the fragment shorthand syntax '<>...</>' instead of the '{{name}}' component."
642
642
  };
643
- const schema$3 = [{
643
+ const schema$4 = [{
644
644
  additionalProperties: false,
645
645
  properties: {
646
646
  fragmentName: {
@@ -672,7 +672,7 @@ function jsxNameToString(node) {
672
672
  }
673
673
  return null;
674
674
  }
675
- function createOnce$9(context) {
675
+ function createOnce$10(context) {
676
676
  let mode;
677
677
  let fragmentName;
678
678
  let namedFragments;
@@ -719,8 +719,8 @@ function createOnce$9(context) {
719
719
  };
720
720
  }
721
721
  const jsxShorthandFragment = createFlawlessRule({
722
- name: RULE_NAME$14,
723
- createOnce: createOnce$9,
722
+ name: RULE_NAME$15,
723
+ createOnce: createOnce$10,
724
724
  defaultOptions: [{
725
725
  fragmentName: DEFAULT_FRAGMENT_NAME,
726
726
  mode: DEFAULT_MODE
@@ -737,8 +737,8 @@ const jsxShorthandFragment = createFlawlessRule({
737
737
  },
738
738
  fixable: "code",
739
739
  hasSuggestions: false,
740
- messages: messages$14,
741
- schema: schema$3,
740
+ messages: messages$15,
741
+ schema: schema$4,
742
742
  type: "suggestion"
743
743
  }
744
744
  });
@@ -877,19 +877,19 @@ function getOpeningParenOfParameters(node, sourceCode) {
877
877
  }
878
878
  //#endregion
879
879
  //#region src/rules/max-lines-per-function/rule.ts
880
- const RULE_NAME$13 = "max-lines-per-function";
880
+ const RULE_NAME$14 = "max-lines-per-function";
881
881
  const MESSAGE_ID_EXCEED = "exceed";
882
882
  /** A line that is empty or holds only whitespace. */
883
883
  const BLANK_LINE = /^\s*$/u;
884
- const DEFAULTS = {
884
+ const DEFAULTS$1 = {
885
885
  countFrom: "body",
886
886
  IIFEs: false,
887
887
  max: 50,
888
888
  skipBlankLines: false,
889
889
  skipComments: false
890
890
  };
891
- const messages$13 = { [MESSAGE_ID_EXCEED]: "{{name}} has too many lines ({{lineCount}}). Maximum allowed is {{maxLines}}." };
892
- const schema$2 = [{
891
+ const messages$14 = { [MESSAGE_ID_EXCEED]: "{{name}} has too many lines ({{lineCount}}). Maximum allowed is {{maxLines}}." };
892
+ const schema$3 = [{
893
893
  additionalProperties: false,
894
894
  properties: {
895
895
  countFrom: {
@@ -991,7 +991,7 @@ function getCountedLoc(countFrom, funcNode, reportNode) {
991
991
  * @param context - The rule context.
992
992
  * @returns The visitors measuring each function.
993
993
  */
994
- function createOnce$8(context) {
994
+ function createOnce$9(context) {
995
995
  let commentLineNumbers;
996
996
  let config;
997
997
  let lines;
@@ -1032,11 +1032,11 @@ function createOnce$8(context) {
1032
1032
  before() {
1033
1033
  const options = context.options[0];
1034
1034
  config = {
1035
- countFrom: options?.countFrom ?? DEFAULTS.countFrom,
1036
- IIFEs: options?.IIFEs ?? DEFAULTS.IIFEs,
1037
- max: options?.max ?? DEFAULTS.max,
1038
- skipBlankLines: options?.skipBlankLines ?? DEFAULTS.skipBlankLines,
1039
- skipComments: options?.skipComments ?? DEFAULTS.skipComments
1035
+ countFrom: options?.countFrom ?? DEFAULTS$1.countFrom,
1036
+ IIFEs: options?.IIFEs ?? DEFAULTS$1.IIFEs,
1037
+ max: options?.max ?? DEFAULTS$1.max,
1038
+ skipBlankLines: options?.skipBlankLines ?? DEFAULTS$1.skipBlankLines,
1039
+ skipComments: options?.skipComments ?? DEFAULTS$1.skipComments
1040
1040
  };
1041
1041
  ({sourceCode} = context);
1042
1042
  lines = [...sourceCode.lines];
@@ -1047,19 +1047,19 @@ function createOnce$8(context) {
1047
1047
  };
1048
1048
  }
1049
1049
  const maxLinesPerFunction = createFlawlessRule({
1050
- name: RULE_NAME$13,
1051
- createOnce: createOnce$8,
1052
- defaultOptions: [DEFAULTS],
1050
+ name: RULE_NAME$14,
1051
+ createOnce: createOnce$9,
1052
+ defaultOptions: [DEFAULTS$1],
1053
1053
  meta: {
1054
- defaultOptions: [DEFAULTS],
1054
+ defaultOptions: [DEFAULTS$1],
1055
1055
  docs: {
1056
1056
  description: "Enforce a maximum number of lines of code in a function",
1057
1057
  recommended: false,
1058
1058
  requiresTypeChecking: false
1059
1059
  },
1060
1060
  hasSuggestions: false,
1061
- messages: messages$13,
1062
- schema: schema$2,
1061
+ messages: messages$14,
1062
+ schema: schema$3,
1063
1063
  type: "suggestion"
1064
1064
  }
1065
1065
  });
@@ -2459,9 +2459,9 @@ const SCHEMA = {
2459
2459
  };
2460
2460
  //#endregion
2461
2461
  //#region src/rules/naming-convention/rule.ts
2462
- const RULE_NAME$12 = "naming-convention";
2462
+ const RULE_NAME$13 = "naming-convention";
2463
2463
  const FOREIGN_CONTRACT_HINT = " If this is data conforming to an external shape, declare it with `satisfies` instead.";
2464
- const messages$12 = {
2464
+ const messages$13 = {
2465
2465
  doesNotMatchFormat: "{{type}} name `{{name}}` must match one of the following formats: {{formats}}",
2466
2466
  doesNotMatchFormatForeignContract: `{{type}} name \`{{name}}\` must match one of the following formats: {{formats}}${FOREIGN_CONTRACT_HINT}`,
2467
2467
  doesNotMatchFormatTrimmed: "{{type}} name `{{name}}` trimmed as `{{processedName}}` must match one of the following formats: {{formats}}",
@@ -2770,7 +2770,7 @@ function create$4(contextWithoutDefaults) {
2770
2770
  }));
2771
2771
  }
2772
2772
  const namingConvention = createEslintRule({
2773
- name: RULE_NAME$12,
2773
+ name: RULE_NAME$13,
2774
2774
  create: create$4,
2775
2775
  defaultOptions: camelCaseNamingConfig,
2776
2776
  meta: {
@@ -2781,7 +2781,7 @@ const namingConvention = createEslintRule({
2781
2781
  },
2782
2782
  fixable: void 0,
2783
2783
  hasSuggestions: false,
2784
- messages: messages$12,
2784
+ messages: messages$13,
2785
2785
  schema: SCHEMA,
2786
2786
  type: "suggestion"
2787
2787
  }
@@ -3022,9 +3022,9 @@ function requiresQuoting(node, target) {
3022
3022
  }
3023
3023
  //#endregion
3024
3024
  //#region src/rules/no-export-default-arrow/rule.ts
3025
- const RULE_NAME$11 = "no-export-default-arrow";
3026
- const MESSAGE_ID$6 = "disallowExportDefaultArrow";
3027
- const messages$11 = { [MESSAGE_ID$6]: "Assign the arrow function to a named constant instead of exporting it anonymously." };
3025
+ const RULE_NAME$12 = "no-export-default-arrow";
3026
+ const MESSAGE_ID$7 = "disallowExportDefaultArrow";
3027
+ const messages$12 = { [MESSAGE_ID$7]: "Assign the arrow function to a named constant instead of exporting it anonymously." };
3028
3028
  /**
3029
3029
  * Converts a filename stem to camelCase, treating `-`, `_`, and whitespace as
3030
3030
  * word separators (`use-mouse` -> `useMouse`).
@@ -3102,7 +3102,7 @@ function createFixFunction({ arrowFunction, context, exportDeclaration, sourceCo
3102
3102
  * @param context - The rule context.
3103
3103
  * @returns The rule listener.
3104
3104
  */
3105
- function createOnce$7(context) {
3105
+ function createOnce$8(context) {
3106
3106
  return { ArrowFunctionExpression(node) {
3107
3107
  const { parent } = node;
3108
3108
  if (parent.type !== AST_NODE_TYPES.ExportDefaultDeclaration) return;
@@ -3113,14 +3113,14 @@ function createOnce$7(context) {
3113
3113
  exportDeclaration: parent,
3114
3114
  sourceCode: context.sourceCode
3115
3115
  }),
3116
- messageId: MESSAGE_ID$6,
3116
+ messageId: MESSAGE_ID$7,
3117
3117
  node
3118
3118
  });
3119
3119
  } };
3120
3120
  }
3121
3121
  const noExportDefaultArrow = createFlawlessRule({
3122
- name: RULE_NAME$11,
3123
- createOnce: createOnce$7,
3122
+ name: RULE_NAME$12,
3123
+ createOnce: createOnce$8,
3124
3124
  defaultOptions: [],
3125
3125
  meta: {
3126
3126
  docs: {
@@ -3130,7 +3130,7 @@ const noExportDefaultArrow = createFlawlessRule({
3130
3130
  },
3131
3131
  fixable: "code",
3132
3132
  hasSuggestions: false,
3133
- messages: messages$11,
3133
+ messages: messages$12,
3134
3134
  schema: [],
3135
3135
  type: "suggestion"
3136
3136
  }
@@ -3295,8 +3295,8 @@ function flatten(file, context) {
3295
3295
  }
3296
3296
  //#endregion
3297
3297
  //#region src/rules/no-redundant-tsconfig-options/rule.ts
3298
- const RULE_NAME$10 = "no-redundant-tsconfig-options";
3299
- const MESSAGE_ID$5 = "redundant";
3298
+ const RULE_NAME$11 = "no-redundant-tsconfig-options";
3299
+ const MESSAGE_ID$6 = "redundant";
3300
3300
  /**
3301
3301
  * Compiler options TypeScript resolves case-insensitively; a child that re-sets
3302
3302
  * one differing only in case is still redundant.
@@ -3331,7 +3331,7 @@ const PATH_KEYS = /* @__PURE__ */ new Set([
3331
3331
  "tsBuildInfoFile",
3332
3332
  "typeRoots"
3333
3333
  ]);
3334
- const messages$10 = { [MESSAGE_ID$5]: "'{{option}}' is redundant: it is already set to this value by {{source}}. Remove it." };
3334
+ const messages$11 = { [MESSAGE_ID$6]: "'{{option}}' is redundant: it is already set to this value by {{source}}. Remove it." };
3335
3335
  /**
3336
3336
  * Structural equality between two static JSON values. Strings compare
3337
3337
  * case-insensitively when `caseInsensitive` is set (used for enum-valued
@@ -3445,7 +3445,7 @@ function create$3(context) {
3445
3445
  },
3446
3446
  fix: buildFix(property),
3447
3447
  loc: property.key.loc,
3448
- messageId: MESSAGE_ID$5
3448
+ messageId: MESSAGE_ID$6
3449
3449
  });
3450
3450
  }
3451
3451
  function buildFix(property) {
@@ -3503,7 +3503,7 @@ function create$3(context) {
3503
3503
  } };
3504
3504
  }
3505
3505
  const noRedundantTsconfigOptions = createEslintRule({
3506
- name: RULE_NAME$10,
3506
+ name: RULE_NAME$11,
3507
3507
  create: create$3,
3508
3508
  defaultOptions: [],
3509
3509
  meta: {
@@ -3514,7 +3514,7 @@ const noRedundantTsconfigOptions = createEslintRule({
3514
3514
  },
3515
3515
  fixable: "code",
3516
3516
  hasSuggestions: false,
3517
- messages: messages$10,
3517
+ messages: messages$11,
3518
3518
  schema: [],
3519
3519
  type: "suggestion"
3520
3520
  }
@@ -3757,15 +3757,15 @@ function resolveFactory(sourceCode, node) {
3757
3757
  }
3758
3758
  //#endregion
3759
3759
  //#region src/rules/no-unnecessary-use-callback/rule.ts
3760
- const RULE_NAME$9 = "no-unnecessary-use-callback";
3760
+ const RULE_NAME$10 = "no-unnecessary-use-callback";
3761
3761
  const MESSAGE_ID_DEFAULT$1 = "default";
3762
3762
  const MESSAGE_ID_INSIDE_USE_EFFECT$1 = "noUnnecessaryUseCallbackInsideUseEffect";
3763
- const messages$9 = {
3763
+ const messages$10 = {
3764
3764
  [MESSAGE_ID_DEFAULT$1]: "An 'useCallback' with empty deps and no references to the component scope may be unnecessary.",
3765
3765
  [MESSAGE_ID_INSIDE_USE_EFFECT$1]: "'{{name}}' is only used inside 1 useEffect, which may be unnecessary. You can move the computation into useEffect directly and merge the dependency arrays."
3766
3766
  };
3767
3767
  const noUnnecessaryUseCallback = createFlawlessRule({
3768
- name: RULE_NAME$9,
3768
+ name: RULE_NAME$10,
3769
3769
  createOnce: createUnnecessaryHookRule({
3770
3770
  hook: "useCallback",
3771
3771
  messageIds: {
@@ -3781,22 +3781,22 @@ const noUnnecessaryUseCallback = createFlawlessRule({
3781
3781
  requiresTypeChecking: false
3782
3782
  },
3783
3783
  hasSuggestions: false,
3784
- messages: messages$9,
3784
+ messages: messages$10,
3785
3785
  schema: [],
3786
3786
  type: "suggestion"
3787
3787
  }
3788
3788
  });
3789
3789
  //#endregion
3790
3790
  //#region src/rules/no-unnecessary-use-memo/rule.ts
3791
- const RULE_NAME$8 = "no-unnecessary-use-memo";
3791
+ const RULE_NAME$9 = "no-unnecessary-use-memo";
3792
3792
  const MESSAGE_ID_DEFAULT = "default";
3793
3793
  const MESSAGE_ID_INSIDE_USE_EFFECT = "noUnnecessaryUseMemoInsideUseEffect";
3794
- const messages$8 = {
3794
+ const messages$9 = {
3795
3795
  [MESSAGE_ID_DEFAULT]: "An 'useMemo' with empty deps and no references to the component scope may be unnecessary.",
3796
3796
  [MESSAGE_ID_INSIDE_USE_EFFECT]: "'{{name}}' is only used inside 1 useEffect, which may be unnecessary. You can move the computation into useEffect directly and merge the dependency arrays."
3797
3797
  };
3798
3798
  const noUnnecessaryUseMemo = createFlawlessRule({
3799
- name: RULE_NAME$8,
3799
+ name: RULE_NAME$9,
3800
3800
  createOnce: createUnnecessaryHookRule({
3801
3801
  hook: "useMemo",
3802
3802
  messageIds: {
@@ -3812,16 +3812,16 @@ const noUnnecessaryUseMemo = createFlawlessRule({
3812
3812
  requiresTypeChecking: false
3813
3813
  },
3814
3814
  hasSuggestions: false,
3815
- messages: messages$8,
3815
+ messages: messages$9,
3816
3816
  schema: [],
3817
3817
  type: "suggestion"
3818
3818
  }
3819
3819
  });
3820
3820
  //#endregion
3821
3821
  //#region src/rules/padding-after-expect-assertions/rule.ts
3822
- const RULE_NAME$7 = "padding-after-expect-assertions";
3823
- const MESSAGE_ID$4 = "missingPadding";
3824
- const messages$7 = { [MESSAGE_ID$4]: "Expected a blank line after '{{name}}'." };
3822
+ const RULE_NAME$8 = "padding-after-expect-assertions";
3823
+ const MESSAGE_ID$5 = "missingPadding";
3824
+ const messages$8 = { [MESSAGE_ID$5]: "Expected a blank line after '{{name}}'." };
3825
3825
  /**
3826
3826
  * Matches a statement that declares the expected assertion count at the top of
3827
3827
  * a test: `expect.assertions(n)` or `expect.hasAssertions()`.
@@ -3896,7 +3896,7 @@ function findPaddingAnchor(sourceCode, node, nextNode) {
3896
3896
  * @param context - The rule context.
3897
3897
  * @returns The rule listener.
3898
3898
  */
3899
- function createOnce$4(context) {
3899
+ function createOnce$5(context) {
3900
3900
  return { ExpressionStatement(node) {
3901
3901
  const name = getAssertionName(node);
3902
3902
  if (name === void 0) return;
@@ -3916,13 +3916,13 @@ function createOnce$4(context) {
3916
3916
  return fixer.insertTextAfter(previousToken, gap === 0 ? "\n\n" : "\n");
3917
3917
  },
3918
3918
  loc: node.loc,
3919
- messageId: MESSAGE_ID$4
3919
+ messageId: MESSAGE_ID$5
3920
3920
  });
3921
3921
  } };
3922
3922
  }
3923
3923
  const paddingAfterExpectAssertions = createFlawlessRule({
3924
- name: RULE_NAME$7,
3925
- createOnce: createOnce$4,
3924
+ name: RULE_NAME$8,
3925
+ createOnce: createOnce$5,
3926
3926
  defaultOptions: [],
3927
3927
  meta: {
3928
3928
  docs: {
@@ -3932,16 +3932,16 @@ const paddingAfterExpectAssertions = createFlawlessRule({
3932
3932
  },
3933
3933
  fixable: "whitespace",
3934
3934
  hasSuggestions: false,
3935
- messages: messages$7,
3935
+ messages: messages$8,
3936
3936
  schema: [],
3937
3937
  type: "layout"
3938
3938
  }
3939
3939
  });
3940
3940
  //#endregion
3941
3941
  //#region src/rules/prefer-destructuring-assignment/rule.ts
3942
- const RULE_NAME$6 = "prefer-destructuring-assignment";
3943
- const MESSAGE_ID$3 = "default";
3944
- const messages$6 = { [MESSAGE_ID$3]: "Use destructuring assignment for component props." };
3942
+ const RULE_NAME$7 = "prefer-destructuring-assignment";
3943
+ const MESSAGE_ID$4 = "default";
3944
+ const messages$7 = { [MESSAGE_ID$4]: "Use destructuring assignment for component props." };
3945
3945
  /**
3946
3946
  * Identifiers that cannot be used as a binding name in strict-mode module code.
3947
3947
  * A property may be accessed with such a name (`props.default`), but a shorthand
@@ -4067,7 +4067,7 @@ function reportComponent(context, scope, propsParameter, propertyVariable) {
4067
4067
  const propertyNames = collectPropertyNames(memberReferences);
4068
4068
  if (!(!hasNonMemberReference && propertyNames !== null && !wouldShadowExistingBinding(context.sourceCode, scope, propertyVariable, memberReferences))) {
4069
4069
  for (const { member } of memberReferences) context.report({
4070
- messageId: MESSAGE_ID$3,
4070
+ messageId: MESSAGE_ID$4,
4071
4071
  node: member
4072
4072
  });
4073
4073
  return;
@@ -4084,7 +4084,7 @@ function reportComponent(context, scope, propsParameter, propertyVariable) {
4084
4084
  if (index === 0) return [fixer.replaceTextRange([propsParameter.range[0], nameEnd], pattern), replaceAccess];
4085
4085
  return replaceAccess;
4086
4086
  },
4087
- messageId: MESSAGE_ID$3,
4087
+ messageId: MESSAGE_ID$4,
4088
4088
  node: member
4089
4089
  });
4090
4090
  }
@@ -4103,7 +4103,7 @@ function reportComponent(context, scope, propsParameter, propertyVariable) {
4103
4103
  * @param context - The rule context.
4104
4104
  * @returns The rule listener.
4105
4105
  */
4106
- function createOnce$3(context) {
4106
+ function createOnce$4(context) {
4107
4107
  let collector = core.getFunctionComponentCollector(context);
4108
4108
  const selectorKeys = Object.keys(collector.visitor);
4109
4109
  const listener = {
@@ -4129,8 +4129,8 @@ function createOnce$3(context) {
4129
4129
  return listener;
4130
4130
  }
4131
4131
  const preferDestructuringAssignment = createFlawlessRule({
4132
- name: RULE_NAME$6,
4133
- createOnce: createOnce$3,
4132
+ name: RULE_NAME$7,
4133
+ createOnce: createOnce$4,
4134
4134
  defaultOptions: [],
4135
4135
  meta: {
4136
4136
  docs: {
@@ -4140,12 +4140,214 @@ const preferDestructuringAssignment = createFlawlessRule({
4140
4140
  },
4141
4141
  fixable: "code",
4142
4142
  hasSuggestions: false,
4143
- messages: messages$6,
4143
+ messages: messages$7,
4144
4144
  schema: [],
4145
4145
  type: "problem"
4146
4146
  }
4147
4147
  });
4148
4148
  //#endregion
4149
+ //#region src/rules/prefer-ending-with-an-expect/rule.ts
4150
+ const RULE_NAME$6 = "prefer-ending-with-an-expect";
4151
+ const MESSAGE_ID$3 = "mustEndWithExpect";
4152
+ const DEFAULTS = {
4153
+ additionalTestBlockFunctions: [],
4154
+ assertFunctionNames: ["expect"]
4155
+ };
4156
+ /** Callee identifiers that name a vitest test block (`describe` is excluded). */
4157
+ const TEST_BLOCK_NAMES = /* @__PURE__ */ new Set(["it", "test"]);
4158
+ const messages$6 = { [MESSAGE_ID$3]: "Test should end with an assertion." };
4159
+ const schema$2 = [{
4160
+ additionalProperties: false,
4161
+ properties: {
4162
+ additionalTestBlockFunctions: {
4163
+ description: "Callee names, besides a resolved vitest it/test, whose second argument is a test body (matched by exact dotted name).",
4164
+ items: { type: "string" },
4165
+ type: "array"
4166
+ },
4167
+ assertFunctionNames: {
4168
+ description: "Function names, besides a resolved vitest expect, that count as an assertion (matched by name; * and ** are wildcards).",
4169
+ items: { type: "string" },
4170
+ type: "array"
4171
+ }
4172
+ },
4173
+ type: "object"
4174
+ }];
4175
+ /**
4176
+ * Tests a dotted callee name against the `assertFunctionNames` patterns. A `*`
4177
+ * stands for a single dotted segment and `**` for any run of segments, so
4178
+ * `request.*.expect` and `request.**.expect` both match a chained assertion.
4179
+ * Ported from eslint-plugin-jest.
4180
+ *
4181
+ * @param nodeName - The dotted callee name (e.g. `expect.toBe`).
4182
+ * @param patterns - The configured assertion-name patterns.
4183
+ * @returns `true` when any pattern matches the name.
4184
+ */
4185
+ function matchesAssertFunctionName(nodeName, patterns) {
4186
+ return patterns.some((pattern) => {
4187
+ return new RegExp(`^${pattern.split(".").map((segment) => {
4188
+ if (segment === "**") return "[a-z\\d\\.]*";
4189
+ return segment.replace(/\*/gu, "[a-z\\d]*");
4190
+ }).join("\\.")}(\\.|$)`, "ui").test(nodeName);
4191
+ });
4192
+ }
4193
+ /**
4194
+ * Builds the dotted name of a callee chain, unwrapping intervening calls
4195
+ * (`request(app).get("/").expect` -> `request.get.expect`) the way
4196
+ * eslint-plugin-jest's `getNodeChain` does. A computed member access yields
4197
+ * `null`, since its property is not a static name.
4198
+ *
4199
+ * @param node - The callee node.
4200
+ * @returns The dotted name, or `null` when it cannot be built statically.
4201
+ */
4202
+ function getNodeName(node) {
4203
+ if (node.type === AST_NODE_TYPES.Identifier) return node.name;
4204
+ if (node.type === AST_NODE_TYPES.CallExpression) return getNodeName(node.callee);
4205
+ if (node.type === AST_NODE_TYPES.MemberExpression && !node.computed && node.property.type === AST_NODE_TYPES.Identifier) {
4206
+ const objectName = getNodeName(node.object);
4207
+ return objectName === null ? null : `${objectName}.${node.property.name}`;
4208
+ }
4209
+ return null;
4210
+ }
4211
+ /**
4212
+ * Walks a callee chain down to the identifier it is rooted at, stepping through
4213
+ * member accesses (`it.each` -> `it`) and intervening calls
4214
+ * (`it.each(cases)()` -> `it`).
4215
+ *
4216
+ * @param node - The callee node.
4217
+ * @returns The root identifier, or `null` when the chain is not rooted at one.
4218
+ */
4219
+ function getRootIdentifier(node) {
4220
+ let current = node;
4221
+ for (;;) {
4222
+ if (current.type === AST_NODE_TYPES.Identifier) return current;
4223
+ if (current.type === AST_NODE_TYPES.CallExpression) {
4224
+ current = current.callee;
4225
+ continue;
4226
+ }
4227
+ if (current.type === AST_NODE_TYPES.MemberExpression) {
4228
+ current = current.object;
4229
+ continue;
4230
+ }
4231
+ return null;
4232
+ }
4233
+ }
4234
+ /**
4235
+ * Resolves the vitest name an identifier refers to, mirroring how
4236
+ * eslint-plugin-jest resolves jest functions. An unresolved reference is a
4237
+ * global (vitest's `globals: true` / `@vitest/globals`); a named import from
4238
+ * `"vitest"` resolves to its imported name (so aliases work); anything bound to
4239
+ * a local variable, function, or parameter resolves to `null` and is ignored.
4240
+ *
4241
+ * @param sourceCode - Provides the scope used to look up the binding.
4242
+ * @param identifier - The identifier to resolve.
4243
+ * @returns The vitest name (`it`/`test`/`expect`/...), or `null` when the
4244
+ * identifier is a local binding rather than a vitest global or import.
4245
+ */
4246
+ function resolveVitestName(sourceCode, identifier) {
4247
+ const variable = findVariable(sourceCode.getScope(identifier), identifier);
4248
+ if (variable === null) return identifier.name;
4249
+ const definition = variable.defs.at(0);
4250
+ if (definition === void 0) return identifier.name;
4251
+ if (definition.type !== DefinitionType.ImportBinding) return null;
4252
+ const importDefinition = definition;
4253
+ const declaration = importDefinition.parent;
4254
+ if (declaration.type !== AST_NODE_TYPES.ImportDeclaration || declaration.source.value !== "vitest") return null;
4255
+ const { node } = importDefinition;
4256
+ if (node.type === AST_NODE_TYPES.ImportSpecifier && node.imported.type === AST_NODE_TYPES.Identifier) return node.imported.name;
4257
+ return null;
4258
+ }
4259
+ /**
4260
+ * Returns the node that ends a test body: the last statement of a block body
4261
+ * (unwrapped to its expression when it is an expression statement), or the
4262
+ * expression of a concise arrow body. Ported from eslint-plugin-jest.
4263
+ *
4264
+ * @param func - The test callback.
4265
+ * @returns The ending node, or `null` for an empty block body.
4266
+ */
4267
+ function getLastStatement(func) {
4268
+ if (func.body.type !== AST_NODE_TYPES.BlockStatement) return func.body;
4269
+ const lastStatement = func.body.body.at(-1);
4270
+ if (lastStatement === void 0) return null;
4271
+ if (lastStatement.type === AST_NODE_TYPES.ExpressionStatement) return lastStatement.expression;
4272
+ return lastStatement;
4273
+ }
4274
+ /**
4275
+ * Flags a test whose last statement is not an assertion, a common sign of an
4276
+ * unfinished test. Ported from eslint-plugin-jest's
4277
+ * `prefer-ending-with-an-expect`, with vitest-aware resolution of `it`/`test`
4278
+ * and `expect`.
4279
+ *
4280
+ * @param context - The rule context.
4281
+ * @returns The rule listener.
4282
+ */
4283
+ function createOnce$3(context) {
4284
+ let config;
4285
+ let sourceCode;
4286
+ /**
4287
+ * Determines whether a call is a test block whose second argument is a body.
4288
+ *
4289
+ * @param node - The call to inspect.
4290
+ * @returns `true` when the call opens a vitest (or configured) test.
4291
+ */
4292
+ function isTestBlock(node) {
4293
+ const root = getRootIdentifier(node.callee);
4294
+ if (root !== null && TEST_BLOCK_NAMES.has(resolveVitestName(sourceCode, root) ?? "")) return true;
4295
+ const name = getNodeName(node.callee);
4296
+ return name !== null && config.additionalTestBlockFunctions.includes(name);
4297
+ }
4298
+ /**
4299
+ * Determines whether a call is an assertion: a resolved vitest `expect`, or a
4300
+ * call whose dotted name matches an `assertFunctionNames` pattern.
4301
+ *
4302
+ * @param node - The call ending the test body.
4303
+ * @returns `true` when the call counts as an assertion.
4304
+ */
4305
+ function isAssertion(node) {
4306
+ const root = getRootIdentifier(node.callee);
4307
+ if (root !== null && resolveVitestName(sourceCode, root) === "expect") return true;
4308
+ return matchesAssertFunctionName(getNodeName(node.callee) ?? "", config.assertFunctionNames);
4309
+ }
4310
+ return {
4311
+ before() {
4312
+ const options = context.options[0];
4313
+ config = {
4314
+ additionalTestBlockFunctions: options?.additionalTestBlockFunctions ?? DEFAULTS.additionalTestBlockFunctions,
4315
+ assertFunctionNames: options?.assertFunctionNames ?? DEFAULTS.assertFunctionNames
4316
+ };
4317
+ ({sourceCode} = context);
4318
+ },
4319
+ CallExpression(node) {
4320
+ if (!isTestBlock(node)) return;
4321
+ const callback = node.arguments[1];
4322
+ if (callback === void 0 || !ASTUtils.isFunction(callback)) return;
4323
+ let lastStatement = getLastStatement(callback);
4324
+ if (lastStatement?.type === AST_NODE_TYPES.AwaitExpression) lastStatement = lastStatement.argument;
4325
+ if (lastStatement?.type === AST_NODE_TYPES.CallExpression && isAssertion(lastStatement)) return;
4326
+ context.report({
4327
+ messageId: MESSAGE_ID$3,
4328
+ node: node.callee
4329
+ });
4330
+ }
4331
+ };
4332
+ }
4333
+ const preferEndingWithAnExpect = createFlawlessRule({
4334
+ name: RULE_NAME$6,
4335
+ createOnce: createOnce$3,
4336
+ defaultOptions: [DEFAULTS],
4337
+ meta: {
4338
+ defaultOptions: [DEFAULTS],
4339
+ docs: {
4340
+ description: "Prefer having the last statement in a test be an assertion",
4341
+ recommended: false,
4342
+ requiresTypeChecking: false
4343
+ },
4344
+ hasSuggestions: false,
4345
+ messages: messages$6,
4346
+ schema: schema$2,
4347
+ type: "suggestion"
4348
+ }
4349
+ });
4350
+ //#endregion
4149
4351
  //#region src/rules/prefer-parameter-destructuring/rule.ts
4150
4352
  const RULE_NAME$5 = "prefer-parameter-destructuring";
4151
4353
  const MESSAGE_ID$2 = "default";
@@ -5201,16 +5403,21 @@ const purity = createFlawlessRule({
5201
5403
  });
5202
5404
  //#endregion
5203
5405
  //#region src/rules/react-namespace/imports.ts
5406
+ /** Matches a bare npm scope (`@rbxts`) with no package segment. */
5407
+ const SCOPE_PATTERN = /^@[^/]+$/;
5204
5408
  /**
5205
- * Whether a module specifier refers to the configured React import source. Matches
5206
- * the exact source or any subpath (`@rbxts/react` and `@rbxts/react/jsx-runtime`).
5409
+ * Whether a module specifier refers to the configured React package. Matches the
5410
+ * exact package or any subpath (`@rbxts/react` and `@rbxts/react/jsx-runtime`),
5411
+ * but never a sibling package (`@rbxts/react-roblox`), because the `/` boundary
5412
+ * excludes it.
5207
5413
  *
5208
5414
  * @param value - The `source.value` of an import declaration.
5209
5415
  * @param importSource - The configured React import source.
5210
5416
  * @returns True when the specifier resolves to the React package.
5211
5417
  */
5212
5418
  function matchesSource(value, importSource) {
5213
- return value === importSource || value.startsWith(`${importSource}/`);
5419
+ const reactPackage = resolveReactPackage(importSource);
5420
+ return value === reactPackage || value.startsWith(`${reactPackage}/`);
5214
5421
  }
5215
5422
  /**
5216
5423
  * Collects the import declarations in a program that come from the React source.
@@ -5339,6 +5546,19 @@ function findNamedSpecifier(declarations, name) {
5339
5546
  };
5340
5547
  return null;
5341
5548
  }
5549
+ /**
5550
+ * Resolves the React package the `react-x` `importSource` setting points at. The
5551
+ * setting may be a package (`react`, `@rbxts/react`) or a bare npm scope
5552
+ * (`@rbxts`, as Roblox configs set it). A scope names no package, so treating it
5553
+ * as one claims every sibling (`@rbxts/react-roblox`, `@rbxts/flux`) as React;
5554
+ * the React package under a scope is `<scope>/react`.
5555
+ *
5556
+ * @param importSource - The configured `react-x` import source.
5557
+ * @returns The React package name to match against.
5558
+ */
5559
+ function resolveReactPackage(importSource) {
5560
+ return SCOPE_PATTERN.test(importSource) ? `${importSource}/react` : importSource;
5561
+ }
5342
5562
  function isNamedSpecifier(specifier) {
5343
5563
  return specifier.type === AST_NODE_TYPES.ImportSpecifier;
5344
5564
  }
@@ -5811,6 +6031,7 @@ const plugin = {
5811
6031
  "no-unnecessary-use-memo": noUnnecessaryUseMemo,
5812
6032
  "padding-after-expect-assertions": paddingAfterExpectAssertions,
5813
6033
  "prefer-destructuring-assignment": preferDestructuringAssignment,
6034
+ "prefer-ending-with-an-expect": preferEndingWithAnExpect,
5814
6035
  "prefer-parameter-destructuring": preferParameterDestructuring,
5815
6036
  "prefer-read-only-props": preferReadOnlyProps,
5816
6037
  "purity": purity,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-flawless",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Your ESLint plugin description",
5
5
  "keywords": [
6
6
  "eslint",