@utilfirst/eslint-plugin 0.2.0 → 0.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.
Files changed (4) hide show
  1. package/LICENSE +3 -2
  2. package/README.md +23 -23
  3. package/dist/index.js +141 -110
  4. package/package.json +1 -1
package/LICENSE CHANGED
@@ -22,8 +22,9 @@ SOFTWARE.
22
22
 
23
23
  Third-party notices
24
24
 
25
- The files under `src/rules/` other than `consistent-blank-lines.ts` and the
26
- files under `src/shared/` include adapted work from anti-slop:
25
+ Portions of this software are adapted from anti-slop
26
+ (https://github.com/dmmulroy/anti-slop) and are used under the following
27
+ license:
27
28
 
28
29
  MIT License
29
30
 
package/README.md CHANGED
@@ -109,29 +109,29 @@ export default [
109
109
 
110
110
  [`docs/rules.md`](./docs/rules.md) explains the policy boundary and expected replacement for every rule. The implementations and colocated tests remain the executable behavior owners.
111
111
 
112
- | Rule | Description |
113
- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
114
- | [`consistent-blank-lines`](./docs/rules/consistent-blank-lines.md) | Insert blank lines between statement-list and JSXChild items that start a new thought |
115
- | [`no-chained-type-assertions`](./docs/rules.md#type-evidence) | Reject chained TypeScript assertions |
116
- | [`no-conditional-undefined-properties`](./docs/rules.md#object-and-api-construction) | Reject conditional undefined object properties |
117
- | [`no-enum-declarations`](./docs/rules.md#object-and-api-construction) | Reject repository-owned enums other than ambient declarations |
118
- | [`no-known-value-widening`](./docs/rules.md#type-evidence) | Reject known values widened into broad target types |
119
- | [`no-module-mocking`](./docs/rules.md#ownership-and-tests) | Reject Vitest and Jest mocking of repository-owned modules |
120
- | [`no-object-parameters`](./docs/rules.md#boundary-contracts) | Reject `object` function parameters |
121
- | [`no-positional-boolean-parameters`](./docs/rules.md#object-and-api-construction) | Reject positional boolean flags on named functions and methods |
122
- | [`no-reflect-apply`](./docs/rules.md#object-and-api-construction) | Reject `Reflect.apply` |
123
- | [`no-reflect-get`](./docs/rules.md#object-and-api-construction) | Reject `Reflect.get` |
124
- | [`no-unknown-parameters`](./docs/rules.md#boundary-contracts) | Keep explicit `unknown` parameters at decoding boundaries |
125
- | [`no-unknown-returns`](./docs/rules.md#boundary-contracts) | Reject `unknown` return contracts |
126
- | [`no-unknown-type-aliases`](./docs/rules.md#boundary-contracts) | Reject type aliases that resolve to `unknown` |
127
- | [`no-unsafe-dictionary-type`](./docs/rules.md#boundary-contracts) | Reject dictionary contracts with broad value types |
128
- | [`no-unhandled-detached-promises`](./docs/rules.md#async-errors) | Require rejection handling on `void`-marked detached call chains |
129
- | [`no-widen-then-assert`](./docs/rules.md#type-evidence) | Reject const flows that widen a known value before narrowing it |
130
- | [`prefer-options-parameter`](./docs/rules.md#object-and-api-construction) | Require options objects for named functions and methods with three or more inputs |
131
- | [`prefer-switch-discriminator-chain`](./docs/rules.md#object-and-api-construction) | Require a switch for four or more equality branches on one discriminator |
132
- | [`prefer-top-level-function-declarations`](./docs/rules.md#object-and-api-construction) | Require declarations for direct top-level function bindings and default exports |
133
- | [`require-lint-suppression-reason`](./docs/rules.md#lint-policy) | Require a forcing reason on lint disable directives |
134
- | [`require-safety-comment-for-type-assertion`](./docs/rules.md#type-evidence) | Require one `SAFETY:` comment for each outermost non-const assertion |
112
+ | Rule | Description |
113
+ | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
114
+ | [`consistent-blank-lines`](./docs/rules/consistent-blank-lines.md) | Apply tight, separate, or preserved gaps between statements and JSX children |
115
+ | [`no-chained-type-assertions`](./docs/rules.md#type-evidence) | Reject chained TypeScript assertions |
116
+ | [`no-conditional-undefined-properties`](./docs/rules.md#object-and-api-construction) | Reject conditional undefined object properties |
117
+ | [`no-enum-declarations`](./docs/rules.md#object-and-api-construction) | Reject repository-owned enums other than ambient declarations |
118
+ | [`no-known-value-widening`](./docs/rules.md#type-evidence) | Reject known values widened into broad target types |
119
+ | [`no-module-mocking`](./docs/rules.md#ownership-and-tests) | Reject Vitest and Jest mocking of repository-owned modules |
120
+ | [`no-object-parameters`](./docs/rules.md#boundary-contracts) | Reject `object` function parameters |
121
+ | [`no-positional-boolean-parameters`](./docs/rules.md#object-and-api-construction) | Reject positional boolean flags on named functions and methods |
122
+ | [`no-reflect-apply`](./docs/rules.md#object-and-api-construction) | Reject `Reflect.apply` |
123
+ | [`no-reflect-get`](./docs/rules.md#object-and-api-construction) | Reject `Reflect.get` |
124
+ | [`no-unknown-parameters`](./docs/rules.md#boundary-contracts) | Keep explicit `unknown` parameters at decoding boundaries |
125
+ | [`no-unknown-returns`](./docs/rules.md#boundary-contracts) | Reject `unknown` return contracts |
126
+ | [`no-unknown-type-aliases`](./docs/rules.md#boundary-contracts) | Reject type aliases that resolve to `unknown` |
127
+ | [`no-unsafe-dictionary-type`](./docs/rules.md#boundary-contracts) | Reject dictionary contracts with broad value types |
128
+ | [`no-unhandled-detached-promises`](./docs/rules.md#async-errors) | Require rejection handling on `void`-marked detached call chains |
129
+ | [`no-widen-then-assert`](./docs/rules.md#type-evidence) | Reject const flows that widen a known value before narrowing it |
130
+ | [`prefer-options-parameter`](./docs/rules.md#object-and-api-construction) | Require options objects for named functions and methods with three or more inputs |
131
+ | [`prefer-switch-discriminator-chain`](./docs/rules.md#object-and-api-construction) | Require a switch for four or more equality branches on one discriminator |
132
+ | [`prefer-top-level-function-declarations`](./docs/rules.md#object-and-api-construction) | Require declarations for direct top-level function bindings and default exports |
133
+ | [`require-lint-suppression-reason`](./docs/rules.md#lint-policy) | Require a forcing reason on lint disable directives |
134
+ | [`require-safety-comment-for-type-assertion`](./docs/rules.md#type-evidence) | Require one `SAFETY:` comment for each outermost non-const assertion |
135
135
 
136
136
  ## Attribution
137
137
 
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { AST_NODE_TYPES } from "@typescript-eslint/utils";
3
3
  import { z } from "zod";
4
4
 
5
5
  //#region package.json
6
- var version = "0.2.0";
6
+ var version = "0.3.0";
7
7
 
8
8
  //#endregion
9
9
  //#region src/rules/consistent-blank-lines.ts
@@ -24,14 +24,8 @@ const IMPORT_SPECIFIER_TYPES = new Set([
24
24
  AST_NODE_TYPES.ImportDefaultSpecifier,
25
25
  AST_NODE_TYPES.ImportNamespaceSpecifier
26
26
  ]);
27
- const OPAQUE_BODY_TYPES = new Set([
28
- AST_NODE_TYPES.FunctionDeclaration,
29
- AST_NODE_TYPES.ClassDeclaration,
30
- AST_NODE_TYPES.ClassExpression
31
- ]);
32
- const FLOW_PREV_TYPES = new Set([AST_NODE_TYPES.ExpressionStatement, AST_NODE_TYPES.IfStatement]);
33
- const FLOW_NEXT_TYPES = new Set([
34
- AST_NODE_TYPES.ExpressionStatement,
27
+ const OPAQUE_BODY_TYPES = new Set([AST_NODE_TYPES.FunctionDeclaration, AST_NODE_TYPES.ClassDeclaration]);
28
+ const EXPRESSION_CONTINUATION_TYPES = new Set([
35
29
  AST_NODE_TYPES.IfStatement,
36
30
  AST_NODE_TYPES.ReturnStatement,
37
31
  AST_NODE_TYPES.ThrowStatement,
@@ -43,6 +37,7 @@ const HEAVY_NEXT_TYPES = new Set([
43
37
  AST_NODE_TYPES.FunctionDeclaration,
44
38
  AST_NODE_TYPES.ClassDeclaration,
45
39
  AST_NODE_TYPES.TSInterfaceDeclaration,
40
+ AST_NODE_TYPES.TSDeclareFunction,
46
41
  AST_NODE_TYPES.ReturnStatement,
47
42
  AST_NODE_TYPES.ThrowStatement
48
43
  ]);
@@ -59,34 +54,42 @@ const consistentBlankLines = {
59
54
  meta: {
60
55
  type: "layout",
61
56
  docs: {
62
- description: "Insert blank lines between statement-list and JSXChild items that start a new thought.",
57
+ description: "Apply explicit gap policies between adjacent statement-list and JSXChild items.",
63
58
  url: "https://github.com/utilfirst/utilfirst-eslint-plugin/blob/main/docs/rules/consistent-blank-lines.md"
64
59
  },
65
60
  fixable: "whitespace",
66
61
  defaultOptions: [],
67
62
  schema: [],
68
63
  messages: {
69
- extra: "Unexpected blank line between items that continue the same paragraph.",
70
- missing: "Expected a blank line between items that start a new paragraph."
64
+ extra: "Unexpected blank line between items that require a tight gap.",
65
+ missing: "Expected one blank line between items that require separation."
71
66
  }
72
67
  },
73
68
  create(context) {
74
69
  const sourceCode = context.sourceCode;
70
+ const lineBreak = sourceCode.text.includes("\r\n") ? "\r\n" : "\n";
75
71
  function checkBlock(statements) {
76
72
  for (let i = 0; i < statements.length - 1; i++) {
77
73
  const prev = statements[i];
78
74
  const next = statements[i + 1];
79
- if (prev && next) checkPair({
80
- effectiveStart: sourceCode.getCommentsBefore(next)[0] ?? next,
81
- isSameParagraph: () => sameParagraph({
75
+ if (prev && next) {
76
+ const { effectiveEnd, leadingComments } = partitionStatementComments({
77
+ comments: sourceCode.getCommentsBefore(next),
78
+ prev
79
+ });
80
+ checkPair({
81
+ effectiveEnd,
82
+ effectiveStart: leadingComments[0] ?? next,
82
83
  next,
83
- prev,
84
- sourceCode
85
- }),
86
- next,
87
- prev,
88
- shouldPreserveExtra: () => isImportPair(prev, next) || isReExport(prev) && isReExport(next) || isExpressionStatementPair(prev, next)
89
- });
84
+ policy: statementGapPolicy({
85
+ leadingComments,
86
+ next,
87
+ prev,
88
+ sourceCode
89
+ }),
90
+ prev
91
+ });
92
+ }
90
93
  }
91
94
  }
92
95
  function checkJsxChildren(children) {
@@ -111,28 +114,36 @@ const consistentBlankLines = {
111
114
  const { node: prev } = prevSibling;
112
115
  const { leading, node: next } = nextSibling;
113
116
  checkPair({
117
+ effectiveEnd: prev,
114
118
  effectiveStart: leading[0] ?? next,
115
- isSameParagraph: () => sameJsxParagraph({
119
+ next,
120
+ policy: jsxGapPolicy({
116
121
  leading,
117
122
  next,
118
123
  prev
119
124
  }),
120
- next,
121
- prev,
122
- shouldPreserveExtra: () => false
125
+ prev
123
126
  });
124
127
  }
125
128
  }
126
129
  }
127
- function checkPair({ effectiveStart, isSameParagraph, next, prev, shouldPreserveExtra }) {
128
- const prevEndLine = prev.loc.end.line;
130
+ function checkPair({ effectiveEnd, effectiveStart, next, policy, prev }) {
131
+ if (policy === "preserve") return;
132
+ const prevEndLine = effectiveEnd.loc.end.line;
129
133
  const nextStartLine = effectiveStart.loc.start.line;
130
- if (nextStartLine - prevEndLine < 1) return;
131
- const isPaddingRequired = !isSameParagraph();
134
+ if (nextStartLine === prevEndLine) {
135
+ if (policy === "tight") return;
136
+ const indentation = indentationOf(prev, sourceCode);
137
+ context.report({
138
+ node: next,
139
+ messageId: "missing",
140
+ fix: (fixer) => fixer.replaceTextRange([effectiveEnd.range[1], effectiveStart.range[0]], `${lineBreak}${lineBreak}${indentation}`)
141
+ });
142
+ return;
143
+ }
132
144
  const paddingCount = nextStartLine - prevEndLine - 1;
133
- const targetPadding = isPaddingRequired ? 1 : 0;
145
+ const targetPadding = policy === "separate" ? 1 : 0;
134
146
  if (paddingCount === targetPadding) return;
135
- if (paddingCount > targetPadding && shouldPreserveExtra()) return;
136
147
  context.report({
137
148
  node: next,
138
149
  messageId: paddingCount < targetPadding ? "missing" : "extra",
@@ -145,7 +156,7 @@ const consistentBlankLines = {
145
156
  line: nextStartLine,
146
157
  column: 0
147
158
  });
148
- return fixer.replaceTextRange([start, end], "\n".repeat(targetPadding));
159
+ return fixer.replaceTextRange([start, end], lineBreak.repeat(targetPadding));
149
160
  }
150
161
  });
151
162
  }
@@ -171,25 +182,61 @@ const consistentBlankLines = {
171
182
  };
172
183
  }
173
184
  };
174
- function sameParagraph({ next, prev, sourceCode }) {
175
- if (hasMultiLineLeadingComment(next, sourceCode)) return false;
176
- const prevHook = isHookStatement(prev);
177
- const nextHook = isHookStatement(next);
178
- if (prevHook || nextHook) return prevHook && nextHook && isMatchingVarDeclPair({
185
+ function partitionStatementComments({ comments, prev }) {
186
+ let effectiveEnd = prev;
187
+ let leadingIndex = 0;
188
+ while (true) {
189
+ const comment = comments[leadingIndex];
190
+ if (!comment || comment.loc.start.line !== effectiveEnd.loc.end.line) break;
191
+ effectiveEnd = comment;
192
+ leadingIndex++;
193
+ }
194
+ return {
195
+ effectiveEnd,
196
+ leadingComments: comments.slice(leadingIndex)
197
+ };
198
+ }
199
+ function indentationOf(node, sourceCode) {
200
+ const lineStart = sourceCode.getIndexFromLoc({
201
+ line: node.loc.start.line,
202
+ column: 0
203
+ });
204
+ const linePrefix = sourceCode.text.slice(lineStart, node.range[0]);
205
+ return /^[\t ]*/u.exec(linePrefix)?.[0] ?? "";
206
+ }
207
+ function statementGapPolicy({ leadingComments, next, prev, sourceCode }) {
208
+ if (itemsSpanMultipleLines(leadingComments)) return "separate";
209
+ const prevHook = hookStatementOf(prev);
210
+ const nextHook = hookStatementOf(next);
211
+ if (prevHook || nextHook) return prevHook?.kind === "variable" && nextHook?.kind === "variable" && declarationsHaveSameShell({
212
+ next,
213
+ prev
214
+ }) ? "tight" : "separate";
215
+ if (isImportPair(prev, next) || isReExport(prev) && isReExport(next) || isExpressionStatementPair(prev, next)) return "preserve";
216
+ if (sharesNameFlow({
179
217
  next,
180
218
  prev,
181
- shouldMatchInitializers: false
182
- });
183
- return sharesNameFlow({
219
+ sourceCode
220
+ }) || statementsRequireTightGap({
184
221
  next,
185
222
  prev,
186
223
  sourceCode
187
- }) || statementsBelongTogether(prev, next);
224
+ })) return "tight";
225
+ return "separate";
188
226
  }
189
- function isHookStatement(stmt) {
190
- if (stmt.type === AST_NODE_TYPES.VariableDeclaration && stmt.declarations.length === 1) return isHookCall(stmt.declarations[0].init);
191
- if (stmt.type === AST_NODE_TYPES.ExpressionStatement) return isHookCall(stmt.expression);
192
- return false;
227
+ function hookStatementOf(stmt) {
228
+ const declaration = unwrapExport(stmt);
229
+ if (declaration.type === AST_NODE_TYPES.VariableDeclaration && declaration.declarations.length === 1 && isHookCall(declaration.declarations[0].init)) return { kind: "variable" };
230
+ if (declaration.type === AST_NODE_TYPES.ExpressionStatement && isHookCall(declaration.expression)) return { kind: "expression" };
231
+ return null;
232
+ }
233
+ function declarationsHaveSameShell({ next, prev }) {
234
+ const previousDeclaration = unwrapExport(prev);
235
+ const nextDeclaration = unwrapExport(next);
236
+ return previousDeclaration.type === AST_NODE_TYPES.VariableDeclaration && nextDeclaration.type === AST_NODE_TYPES.VariableDeclaration && isExported(prev) === isExported(next) && !isMultiLine(prev) && !isMultiLine(next) && previousDeclaration.declarations.length === 1 && nextDeclaration.declarations.length === 1;
237
+ }
238
+ function isExported(stmt) {
239
+ return stmt.type === AST_NODE_TYPES.ExportNamedDeclaration || stmt.type === AST_NODE_TYPES.ExportDefaultDeclaration;
193
240
  }
194
241
  function sharesNameFlow({ next, prev, sourceCode }) {
195
242
  if (isMultiLine(prev)) return false;
@@ -205,21 +252,17 @@ function sharesNameFlow({ next, prev, sourceCode }) {
205
252
  function isHookCall(node) {
206
253
  return node?.type === AST_NODE_TYPES.CallExpression && node.callee.type === AST_NODE_TYPES.Identifier && /^use[A-Z]/u.test(node.callee.name);
207
254
  }
208
- function statementsBelongTogether(prev, next) {
209
- if (isImportPair(prev, next)) return true;
210
- if (isReExport(prev) && isReExport(next)) return true;
255
+ function statementsRequireTightGap({ next, prev, sourceCode }) {
211
256
  if (isMatchingVarDeclPair({
212
257
  next,
213
- prev
258
+ prev,
259
+ sourceCode
214
260
  })) return true;
215
261
  if (isMatchingTypeAliasPair(prev, next)) return true;
216
- if (FLOW_PREV_TYPES.has(prev.type) && FLOW_NEXT_TYPES.has(next.type)) {
262
+ if (prev.type === AST_NODE_TYPES.IfStatement && next.type === AST_NODE_TYPES.IfStatement) return !(isGuardIf(prev) && !isGuardIf(next));
263
+ if (prev.type === AST_NODE_TYPES.ExpressionStatement && EXPRESSION_CONTINUATION_TYPES.has(next.type)) {
217
264
  if (HEAVY_NEXT_TYPES.has(next.type) && isMultiLine(next)) return false;
218
- if (prev.type === AST_NODE_TYPES.IfStatement) {
219
- if (next.type !== AST_NODE_TYPES.IfStatement) return false;
220
- if (isGuardIf(prev) && !isGuardIf(next)) return false;
221
- } else if (next.type === AST_NODE_TYPES.IfStatement && isGuardIf(next)) return false;
222
- return true;
265
+ return !(next.type === AST_NODE_TYPES.IfStatement && isGuardIf(next));
223
266
  }
224
267
  return false;
225
268
  }
@@ -232,56 +275,47 @@ function isExpressionStatementPair(prev, next) {
232
275
  function isReExport(stmt) {
233
276
  return stmt.type === AST_NODE_TYPES.ExportNamedDeclaration && stmt.source !== null || stmt.type === AST_NODE_TYPES.ExportAllDeclaration;
234
277
  }
235
- function isMatchingVarDeclPair({ next, prev, shouldMatchInitializers = true }) {
278
+ function isMatchingVarDeclPair({ next, prev, sourceCode }) {
236
279
  const previousDeclaration = unwrapExport(prev);
237
280
  const nextDeclaration = unwrapExport(next);
238
281
  if (previousDeclaration.type !== AST_NODE_TYPES.VariableDeclaration || nextDeclaration.type !== AST_NODE_TYPES.VariableDeclaration) return false;
239
282
  if (!isConstOrLet(previousDeclaration) || !isConstOrLet(nextDeclaration)) return false;
240
283
  if (isMultiLine(prev) || isMultiLine(next)) return false;
241
284
  if (previousDeclaration.declarations.length !== 1 || nextDeclaration.declarations.length !== 1) return false;
242
- if (shouldMatchInitializers && !initializersBelongTogether(previousDeclaration.declarations[0].init, nextDeclaration.declarations[0].init)) return false;
243
- return prev.type === AST_NODE_TYPES.ExportNamedDeclaration === (next.type === AST_NODE_TYPES.ExportNamedDeclaration);
285
+ if (!initializersBelongTogether({
286
+ nextInit: nextDeclaration.declarations[0].init,
287
+ prevInit: previousDeclaration.declarations[0].init,
288
+ sourceCode
289
+ })) return false;
290
+ return isExported(prev) === isExported(next);
244
291
  }
245
292
  function isMatchingTypeAliasPair(prev, next) {
246
293
  const previousDeclaration = unwrapExport(prev);
247
294
  const nextDeclaration = unwrapExport(next);
248
295
  if (previousDeclaration.type !== AST_NODE_TYPES.TSTypeAliasDeclaration || nextDeclaration.type !== AST_NODE_TYPES.TSTypeAliasDeclaration) return false;
249
296
  if (isMultiLine(prev) || isMultiLine(next)) return false;
250
- return prev.type === AST_NODE_TYPES.ExportNamedDeclaration === (next.type === AST_NODE_TYPES.ExportNamedDeclaration);
297
+ return isExported(prev) === isExported(next);
251
298
  }
252
299
  function unwrapExport(stmt) {
253
300
  if (stmt.type === AST_NODE_TYPES.ExportNamedDeclaration && stmt.declaration) return stmt.declaration;
301
+ if (stmt.type === AST_NODE_TYPES.ExportDefaultDeclaration) return stmt.declaration;
254
302
  return stmt;
255
303
  }
256
304
  function isConstOrLet(decl) {
257
305
  return decl.kind === "const" || decl.kind === "let";
258
306
  }
259
- function initializersBelongTogether(prevInit, nextInit) {
260
- const prevCall = asCall(prevInit);
261
- const nextCall = asCall(nextInit);
262
- if (prevCall && nextCall) return calleesEqual(prevCall.callee, nextCall.callee);
263
- if (prevCall || nextCall) return false;
307
+ function initializersBelongTogether({ nextInit, prevInit, sourceCode }) {
308
+ const previousCallIdentity = callIdentityOf(prevInit, sourceCode);
309
+ const nextCallIdentity = callIdentityOf(nextInit, sourceCode);
310
+ if (previousCallIdentity !== null && nextCallIdentity !== null) return previousCallIdentity === nextCallIdentity;
311
+ if (previousCallIdentity !== null || nextCallIdentity !== null) return false;
264
312
  return true;
265
313
  }
266
- function asCall(expr) {
314
+ function callIdentityOf(expr, sourceCode) {
267
315
  if (!expr) return null;
268
- if (expr.type === AST_NODE_TYPES.CallExpression) return expr;
269
- return null;
270
- }
271
- function calleesEqual(a, b) {
272
- if (!a || a.type !== b?.type) return false;
273
- if (a.type === AST_NODE_TYPES.Identifier && b.type === AST_NODE_TYPES.Identifier) return a.name === b.name;
274
- if (a.type === AST_NODE_TYPES.ThisExpression) return true;
275
- if (a.type === AST_NODE_TYPES.MemberExpression && b.type === AST_NODE_TYPES.MemberExpression) {
276
- if (a.computed !== b.computed) return false;
277
- if (!calleesEqual(a.object, b.object)) return false;
278
- if (!a.computed && a.property.type === AST_NODE_TYPES.Identifier && b.property.type === AST_NODE_TYPES.Identifier) return a.property.name === b.property.name;
279
- if (a.computed && a.property.type === AST_NODE_TYPES.Identifier && b.property.type === AST_NODE_TYPES.Identifier) return a.property.name === b.property.name;
280
- if (a.computed && a.property.type === AST_NODE_TYPES.Literal && b.property.type === AST_NODE_TYPES.Literal) return a.property.value === b.property.value;
281
- return false;
282
- }
283
- if (a.type === AST_NODE_TYPES.CallExpression && b.type === AST_NODE_TYPES.CallExpression) return calleesEqual(a.callee, b.callee);
284
- return false;
316
+ const call = expr.type === AST_NODE_TYPES.ChainExpression ? expr.expression : expr;
317
+ if (call.type !== AST_NODE_TYPES.CallExpression) return null;
318
+ return `${call.optional ? "optional" : "direct"}:${sourceCode.getText(call.callee)}`;
285
319
  }
286
320
  function isGuardIf(stmt) {
287
321
  return stmt.type === AST_NODE_TYPES.IfStatement && blockAlwaysTerminates(stmt.consequent);
@@ -311,6 +345,7 @@ function collectIntroducedOrAssignedNames(stmt) {
311
345
  else if (s.type === AST_NODE_TYPES.FunctionDeclaration && s.id) set.add(s.id.name);
312
346
  else if (s.type === AST_NODE_TYPES.ClassDeclaration && s.id) set.add(s.id.name);
313
347
  else if (s.type === AST_NODE_TYPES.TSInterfaceDeclaration) set.add(s.id.name);
348
+ else if (s.type === AST_NODE_TYPES.TSDeclareFunction && s.id) set.add(s.id.name);
314
349
  else if (s.type === AST_NODE_TYPES.ExpressionStatement) {
315
350
  const expr = s.expression;
316
351
  if (expr.type === AST_NODE_TYPES.AssignmentExpression) collectAssignmentRoots(expr.left, set);
@@ -405,11 +440,17 @@ function isInSubtree(node, root) {
405
440
  }
406
441
  function isDeclarationOrPropertyKey(idNode) {
407
442
  const parent = idNode.parent;
443
+ if (parent.type === AST_NODE_TYPES.LabeledStatement && parent.label === idNode || (parent.type === AST_NODE_TYPES.BreakStatement || parent.type === AST_NODE_TYPES.ContinueStatement) && parent.label === idNode) return true;
408
444
  if (parent.type === AST_NODE_TYPES.VariableDeclarator && parent.id === idNode) return true;
409
445
  if (FN_DECL_TYPES.has(parent.type) && "id" in parent && parent.id === idNode) return true;
410
446
  if ((parent.type === AST_NODE_TYPES.ClassDeclaration || parent.type === AST_NODE_TYPES.ClassExpression) && parent.id === idNode) return true;
411
447
  if (parent.type === AST_NODE_TYPES.MemberExpression && parent.property === idNode && !parent.computed) return true;
412
448
  if (IMPORT_SPECIFIER_TYPES.has(parent.type) && "local" in parent && parent.local === idNode) return true;
449
+ if (parent.type === AST_NODE_TYPES.ImportSpecifier) return true;
450
+ if (parent.type === AST_NODE_TYPES.ExportSpecifier && parent.exported === idNode && parent.local !== idNode) return true;
451
+ if (parent.type === AST_NODE_TYPES.MetaProperty) return true;
452
+ if (parent.type === AST_NODE_TYPES.TSQualifiedName && parent.right === idNode) return true;
453
+ if ((parent.type === AST_NODE_TYPES.MethodDefinition || parent.type === AST_NODE_TYPES.PropertyDefinition || parent.type === AST_NODE_TYPES.AccessorProperty || parent.type === AST_NODE_TYPES.TSMethodSignature || parent.type === AST_NODE_TYPES.TSPropertySignature) && parent.key === idNode && !parent.computed) return true;
413
454
  if (parent.type === AST_NODE_TYPES.Property && parent.key === idNode && !parent.computed && !parent.shorthand) return true;
414
455
  if (isInBindingPosition(idNode)) return true;
415
456
  return false;
@@ -443,35 +484,26 @@ function isInBindingPosition(idNode) {
443
484
  }
444
485
  return false;
445
486
  }
446
- function sameJsxParagraph({ leading, next, prev }) {
447
- const firstLeading = leading[0];
448
- const lastLeading = leading.at(-1);
449
- if (firstLeading && lastLeading && lastLeading.loc.end.line > firstLeading.loc.start.line) return false;
450
- if (siblingsIncludeLiteralText(next)) return true;
451
- return !(isMultiLine(prev) || isMultiLine(next));
487
+ function jsxGapPolicy({ leading, next, prev }) {
488
+ if (itemsSpanMultipleLines(leading)) return "separate";
489
+ if (isLiteralTextChild(prev) || isLiteralTextChild(next)) return "tight";
490
+ return isMultiLine(prev) || isMultiLine(next) ? "separate" : "tight";
452
491
  }
453
492
  function isCommentOnlyContainer(child) {
454
493
  return child.type === AST_NODE_TYPES.JSXExpressionContainer && child.expression.type === AST_NODE_TYPES.JSXEmptyExpression;
455
494
  }
456
- function siblingsIncludeLiteralText(node) {
457
- const parent = parentOf(node);
458
- if (!parent || parent.type !== AST_NODE_TYPES.JSXElement && parent.type !== AST_NODE_TYPES.JSXFragment) return false;
459
- for (const sibling of parent.children) {
460
- if (sibling.type === AST_NODE_TYPES.JSXText && sibling.value.trim() !== "") return true;
461
- if (sibling.type === AST_NODE_TYPES.JSXExpressionContainer && expressionYieldsStringLiteral(sibling.expression)) return true;
462
- }
463
- return false;
495
+ function isLiteralTextChild(child) {
496
+ if (child.type === AST_NODE_TYPES.JSXText) return child.value.trim() !== "";
497
+ return child.type === AST_NODE_TYPES.JSXExpressionContainer && expressionProducesTextOrNothing(child.expression);
464
498
  }
465
- function expressionYieldsStringLiteral(expr) {
466
- if (expr.type === AST_NODE_TYPES.Literal) return isString$2(expr.value);
499
+ function expressionProducesTextOrNothing(expr) {
500
+ if (expr.type === AST_NODE_TYPES.Literal) return typeof expr.value === "string";
467
501
  if (expr.type === AST_NODE_TYPES.TemplateLiteral) return true;
468
- if (expr.type === AST_NODE_TYPES.LogicalExpression) return expressionYieldsStringLiteral(expr.right);
469
- if (expr.type === AST_NODE_TYPES.ConditionalExpression) return expressionYieldsStringLiteral(expr.consequent) || expressionYieldsStringLiteral(expr.alternate);
502
+ if (expr.type === AST_NODE_TYPES.TSAsExpression || expr.type === AST_NODE_TYPES.TSTypeAssertion || expr.type === AST_NODE_TYPES.TSNonNullExpression || expr.type === AST_NODE_TYPES.TSSatisfiesExpression) return expressionProducesTextOrNothing(expr.expression);
503
+ if (expr.type === AST_NODE_TYPES.LogicalExpression && expr.operator === "&&") return expressionProducesTextOrNothing(expr.right);
504
+ if (expr.type === AST_NODE_TYPES.ConditionalExpression) return expressionProducesTextOrNothing(expr.consequent) && expressionProducesTextOrNothing(expr.alternate);
470
505
  return false;
471
506
  }
472
- function isString$2(value) {
473
- return typeof value === "string";
474
- }
475
507
  function isNode(value) {
476
508
  return value !== null && typeof value === "object" && "type" in value && typeof value.type === "string";
477
509
  }
@@ -492,12 +524,11 @@ function walk(node, fn) {
492
524
  function isMultiLine(node) {
493
525
  return node.loc.start.line !== node.loc.end.line;
494
526
  }
495
- function hasMultiLineLeadingComment(node, sourceCode) {
496
- const comments = sourceCode.getCommentsBefore(node);
497
- if (comments.length === 0) return false;
498
- const first = comments[0];
499
- const last = comments.at(-1);
500
- return Boolean(first && last && last.loc.end.line > first.loc.start.line);
527
+ function itemsSpanMultipleLines(items) {
528
+ const first = items[0];
529
+ const last = items.at(-1);
530
+ if (!first || !last) return false;
531
+ return last.loc.end.line > first.loc.start.line;
501
532
  }
502
533
 
503
534
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilfirst/eslint-plugin",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Shared lint rules for ESLint and Oxlint",
5
5
  "keywords": [
6
6
  "eslint",