eslint-plugin-flawless 1.4.1 → 1.4.3

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.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as plugin, t as PLUGIN_NAME } from "./plugin-p3_AxnS4.mjs";
1
+ import { n as plugin, t as PLUGIN_NAME } from "./plugin-rI_n00SA.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-p3_AxnS4.mjs";
1
+ import { n as plugin, t as PLUGIN_NAME } from "./plugin-rI_n00SA.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.4.1";
17
+ var version = "1.4.3";
18
18
  var repository = {
19
19
  "url": "git+https://github.com/christopher-buss/eslint-plugin-flawless.git",
20
20
  "type": "git"
@@ -3030,6 +3030,11 @@ const DEFAULTS$1 = {
3030
3030
  };
3031
3031
  /** Callee identifiers that name a vitest test block (`describe` is excluded). */
3032
3032
  const TEST_BLOCK_NAMES$1 = /* @__PURE__ */ new Set(["it", "test"]);
3033
+ /**
3034
+ * Root identifiers whose call arguments are assertion conditions. Matched by
3035
+ * name as well as by vitest resolution, so `assert` from `node:assert` counts.
3036
+ */
3037
+ const ASSERTION_ROOT_NAMES = /* @__PURE__ */ new Set(["assert", "expect"]);
3033
3038
  const messages$14 = { [MESSAGE_ID$9]: "Avoid having conditionals in tests." };
3034
3039
  const schema$3 = [{
3035
3040
  additionalProperties: false,
@@ -3113,12 +3118,43 @@ function resolveVitestName$1(sourceCode, identifier) {
3113
3118
  return null;
3114
3119
  }
3115
3120
  /**
3116
- * Collects the `?.` tokens along an optional chain's primary spine and the text
3117
- * that replaces each with a non-null assertion. A non-computed member becomes
3118
- * `!.` (`a?.b` -> `a!.b`); a computed member or optional call becomes `!`
3119
- * (`a?.[x]` -> `a![x]`, `fn?.()` -> `fn!()`). Only the object/callee spine is
3120
- * walked optional chains inside computed keys or call arguments are their own
3121
- * `ChainExpression` nodes and are visited (and fixed) separately.
3121
+ * Determines whether a node sits directly in the argument list of an assertion
3122
+ * call (`expect(...)`, `assert(...)`, `assert.ok(...)`, `expect(x).toBe(...)`).
3123
+ * The search walks up to the nearest enclosing call and stops there, so an
3124
+ * argument of some other function nested inside an assertion
3125
+ * (`expect(wrap(a && b))`) does not qualify; it also stops at a function
3126
+ * boundary, since a callback's body runs on the callback's terms rather than as
3127
+ * part of the assertion's condition.
3128
+ *
3129
+ * @param node - The node to locate.
3130
+ * @param sourceCode - Provides the scope used to resolve the callee.
3131
+ * @returns `true` when the node is an argument of an assertion call.
3132
+ */
3133
+ function isInAssertionArgument(node, sourceCode) {
3134
+ let current = node;
3135
+ while (current.type !== AST_NODE_TYPES.Program) {
3136
+ const { parent } = current;
3137
+ if (parent.type === AST_NODE_TYPES.ArrowFunctionExpression || parent.type === AST_NODE_TYPES.FunctionDeclaration || parent.type === AST_NODE_TYPES.FunctionExpression) return false;
3138
+ if (parent.type === AST_NODE_TYPES.CallExpression) {
3139
+ if (current === parent.callee) return false;
3140
+ const root = getRootIdentifier$1(parent.callee);
3141
+ if (root === null) return false;
3142
+ return ASSERTION_ROOT_NAMES.has(root.name) || ASSERTION_ROOT_NAMES.has(resolveVitestName$1(sourceCode, root) ?? "");
3143
+ }
3144
+ current = parent;
3145
+ }
3146
+ return false;
3147
+ }
3148
+ /**
3149
+ * Collects the `?.` tokens along an optional chain's primary spine and the parts
3150
+ * that convert each into a non-null assertion. The `!` is anchored to the token
3151
+ * preceding `?.` rather than written in place of it, because TypeScript forbids
3152
+ * a line break before `!` — a chain wrapped as `a\n?.b` has to become `a!\n.b`,
3153
+ * not `a\n!.b`. What replaces `?.` is therefore only the connector: `.` for a
3154
+ * plain member (`a?.b` -> `a!.b`), nothing for a computed member or optional
3155
+ * call (`a?.[x]` -> `a![x]`, `fn?.()` -> `fn!()`). Only the object/callee spine
3156
+ * is walked — optional chains inside computed keys or call arguments are their
3157
+ * own `ChainExpression` nodes and are visited (and fixed) separately.
3122
3158
  *
3123
3159
  * @param chain - The chain expression to convert.
3124
3160
  * @param sourceCode - Provides token lookups.
@@ -3131,8 +3167,10 @@ function collectOptionalTokenFixes(chain, sourceCode) {
3131
3167
  if (node.type === AST_NODE_TYPES.MemberExpression) {
3132
3168
  if (node.optional) {
3133
3169
  const token = sourceCode.getTokenAfter(node.object, { filter: (candidate) => candidate.value === "?." });
3134
- if (token !== null) fixes.push({
3135
- text: node.computed ? "!" : "!.",
3170
+ const anchor = token === null ? null : sourceCode.getTokenBefore(token);
3171
+ if (token !== null && anchor !== null) fixes.push({
3172
+ anchor,
3173
+ text: node.computed ? "" : ".",
3136
3174
  token
3137
3175
  });
3138
3176
  }
@@ -3142,8 +3180,10 @@ function collectOptionalTokenFixes(chain, sourceCode) {
3142
3180
  if (node.type === AST_NODE_TYPES.CallExpression) {
3143
3181
  if (node.optional) {
3144
3182
  const token = sourceCode.getTokenAfter(node.callee, { filter: (candidate) => candidate.value === "?." });
3145
- if (token !== null) fixes.push({
3146
- text: "!",
3183
+ const anchor = token === null ? null : sourceCode.getTokenBefore(token);
3184
+ if (token !== null && anchor !== null) fixes.push({
3185
+ anchor,
3186
+ text: "",
3147
3187
  token
3148
3188
  });
3149
3189
  }
@@ -3165,13 +3205,13 @@ function collectOptionalTokenFixes(chain, sourceCode) {
3165
3205
  function createOnce$10(context) {
3166
3206
  let config;
3167
3207
  let sourceCode;
3168
- let inTestCase = false;
3208
+ const testBlocks = [];
3169
3209
  /**
3170
- * Determines whether a call opens a test block (a resolved vitest `it`/`test`,
3210
+ * Determines whether a call names a test block (a resolved vitest `it`/`test`,
3171
3211
  * or a configured `additionalTestBlockFunctions` name).
3172
3212
  *
3173
3213
  * @param node - The call to inspect.
3174
- * @returns `true` when the call opens a test.
3214
+ * @returns `true` when the call is rooted at a test block name.
3175
3215
  */
3176
3216
  function isTestBlock(node) {
3177
3217
  const root = getRootIdentifier$1(node.callee);
@@ -3180,12 +3220,46 @@ function createOnce$10(context) {
3180
3220
  return name !== null && config.additionalTestBlockFunctions.includes(name);
3181
3221
  }
3182
3222
  /**
3223
+ * Determines whether a test block call is a modifier applied to the block
3224
+ * rather than the call that opens it. In `it.skipIf(cond)("works", fn)` both
3225
+ * calls are rooted at `it`, but only the outer one takes the test body — the
3226
+ * inner one sits in its callee position.
3227
+ *
3228
+ * @param node - The test block call to inspect.
3229
+ * @returns `true` when the call is a modifier in a callee chain.
3230
+ */
3231
+ function isTestBlockModifier(node) {
3232
+ const { parent } = node;
3233
+ return parent.type === AST_NODE_TYPES.CallExpression && parent.callee === node;
3234
+ }
3235
+ /**
3236
+ * Determines whether a node sits in the body of the innermost open test block.
3237
+ * A node reached through that block's callee — a modifier's arguments
3238
+ * (`it.skipIf(cond)`, `it.each(cases)`) or a computed key (`it[flag ? …]`) —
3239
+ * decides whether and how the test runs rather than what it does, so it is not
3240
+ * part of the body.
3241
+ *
3242
+ * @param node - The node to locate.
3243
+ * @returns `true` when the node is inside a test body.
3244
+ */
3245
+ function isInTestBody(node) {
3246
+ const block = testBlocks.at(-1);
3247
+ if (block === void 0) return false;
3248
+ let current = node;
3249
+ while (current.type !== AST_NODE_TYPES.Program) {
3250
+ const { parent } = current;
3251
+ if (parent === block) return current !== block.callee;
3252
+ current = parent;
3253
+ }
3254
+ return false;
3255
+ }
3256
+ /**
3183
3257
  * Reports a conditional node when the traversal is inside a test body.
3184
3258
  *
3185
3259
  * @param node - The `if`/`switch`/ternary/logical construct to flag.
3186
3260
  */
3187
3261
  function maybeReportConditional(node) {
3188
- if (inTestCase) context.report({
3262
+ if (isInTestBody(node)) context.report({
3189
3263
  messageId: MESSAGE_ID$9,
3190
3264
  node
3191
3265
  });
@@ -3198,20 +3272,20 @@ function createOnce$10(context) {
3198
3272
  allowOptionalChaining: options?.allowOptionalChaining ?? DEFAULTS$1.allowOptionalChaining
3199
3273
  };
3200
3274
  ({sourceCode} = context);
3201
- inTestCase = false;
3275
+ testBlocks.length = 0;
3202
3276
  },
3203
3277
  "CallExpression": function(node) {
3204
- if (isTestBlock(node)) inTestCase = true;
3278
+ if (isTestBlock(node) && !isTestBlockModifier(node)) testBlocks.push(node);
3205
3279
  },
3206
3280
  "CallExpression:exit": function(node) {
3207
- if (isTestBlock(node)) inTestCase = false;
3281
+ if (testBlocks.at(-1) === node) testBlocks.pop();
3208
3282
  },
3209
3283
  "ChainExpression": function(node) {
3210
- if (!inTestCase || config.allowOptionalChaining) return;
3284
+ if (config.allowOptionalChaining || !isInTestBody(node)) return;
3211
3285
  context.report({
3212
3286
  fix: (fixer) => {
3213
- return collectOptionalTokenFixes(node, sourceCode).map((optionalFix) => {
3214
- return fixer.replaceText(optionalFix.token, optionalFix.text);
3287
+ return collectOptionalTokenFixes(node, sourceCode).flatMap((optionalFix) => {
3288
+ return [fixer.insertTextAfter(optionalFix.anchor, "!"), fixer.replaceText(optionalFix.token, optionalFix.text)];
3215
3289
  });
3216
3290
  },
3217
3291
  messageId: MESSAGE_ID$9,
@@ -3220,7 +3294,10 @@ function createOnce$10(context) {
3220
3294
  },
3221
3295
  "ConditionalExpression": maybeReportConditional,
3222
3296
  "IfStatement": maybeReportConditional,
3223
- "LogicalExpression": maybeReportConditional,
3297
+ "LogicalExpression": function(node) {
3298
+ if (node.operator === "&&" && isInAssertionArgument(node, sourceCode)) return;
3299
+ maybeReportConditional(node);
3300
+ },
3224
3301
  "SwitchStatement": maybeReportConditional
3225
3302
  };
3226
3303
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-flawless",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
4
4
  "description": "Your ESLint plugin description",
5
5
  "keywords": [
6
6
  "eslint",