@blumintinc/eslint-plugin-blumint 1.20.80 → 1.20.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.80',
226
+ version: '1.20.81',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -156,6 +156,44 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
156
156
  }
157
157
  return null;
158
158
  };
159
+ /**
160
+ * The props type carried by a component annotation on the DECLARATOR —
161
+ * `const C: React.FC<X> = ({ ... }) => ...` — used only when the parameter
162
+ * itself is unannotated (#1620). Gated to FC-shaped annotations so an
163
+ * arbitrary declarator type is never mistaken for a props source; the
164
+ * returned argument runs through the same resolver as a direct annotation,
165
+ * so wrappers like `React.FC<Readonly<XProps>>` resolve for free.
166
+ */
167
+ const FC_TYPE_NAMES = new Set([
168
+ 'FC',
169
+ 'FunctionComponent',
170
+ 'VFC',
171
+ 'VoidFunctionComponent',
172
+ ]);
173
+ const propsArgumentOfFcAnnotation = (declarationId) => {
174
+ if (declarationId.type !== utils_1.AST_NODE_TYPES.Identifier) {
175
+ return undefined;
176
+ }
177
+ const annotation = declarationId.typeAnnotation?.typeAnnotation;
178
+ if (annotation?.type !== utils_1.AST_NODE_TYPES.TSTypeReference) {
179
+ return undefined;
180
+ }
181
+ const { typeName } = annotation;
182
+ const localName = typeName.type === utils_1.AST_NODE_TYPES.Identifier
183
+ ? typeName.name
184
+ : typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName &&
185
+ typeName.left.type === utils_1.AST_NODE_TYPES.Identifier &&
186
+ typeName.left.name === 'React' &&
187
+ typeName.right.type === utils_1.AST_NODE_TYPES.Identifier
188
+ ? typeName.right.name
189
+ : null;
190
+ if (localName === null || !FC_TYPE_NAMES.has(localName)) {
191
+ return undefined;
192
+ }
193
+ const typeArgs = annotation.typeParameters ??
194
+ annotation.typeArguments;
195
+ return typeArgs?.params[0];
196
+ };
159
197
  const isAnyPropFromSpreadTypeUsed = (spreadTypeName, used, knownProps) => {
160
198
  const spreadTypeProps = spreadTypeToPropNames.get(spreadTypeName);
161
199
  if (!spreadTypeProps || spreadTypeProps.size === 0) {
@@ -769,8 +807,10 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
769
807
  const param = fn.params[0];
770
808
  if (param?.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
771
809
  // Resolve through generic wrappers (e.g. `Readonly<FooProps>`) as well
772
- // as the plain `FooProps` annotation.
773
- const typeName = resolvePropsTypeName(param.typeAnnotation?.typeAnnotation);
810
+ // as the plain `FooProps` annotation; an unannotated pattern under an
811
+ // FC-annotated declarator resolves from the annotation's argument.
812
+ const typeName = resolvePropsTypeName(param.typeAnnotation?.typeAnnotation ??
813
+ propsArgumentOfFcAnnotation(declaration.id));
774
814
  if (typeName) {
775
815
  const used = new Set();
776
816
  const restUsed = collectUsedFromObjectPattern(param, typeName, used);
@@ -782,7 +822,8 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
782
822
  // Identifier param (`props: FooProps`): the destructuring happens in
783
823
  // the body. Resolve the Props type (unwrapping generic wrappers) and
784
824
  // scan the body for `const { ... } = props`.
785
- const typeName = resolvePropsTypeName(param.typeAnnotation?.typeAnnotation);
825
+ const typeName = resolvePropsTypeName(param.typeAnnotation?.typeAnnotation ??
826
+ propsArgumentOfFcAnnotation(declaration.id));
786
827
  if (!typeName) {
787
828
  return;
788
829
  }
@@ -9,6 +9,59 @@ const defaultOptions = [
9
9
  enforceForRenamedProperties: false,
10
10
  },
11
11
  ];
12
+ /**
13
+ * Names of every class declared anywhere in the file. A purely syntactic rule
14
+ * cannot see an imported class, so same-file declarations are the entire
15
+ * population a type annotation can be resolved against (#1619).
16
+ */
17
+ function collectClassNames(sourceCode) {
18
+ const names = new Set();
19
+ const stack = [sourceCode.ast];
20
+ while (stack.length > 0) {
21
+ const current = stack.pop();
22
+ if ((current.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
23
+ current.type === utils_1.AST_NODE_TYPES.ClassExpression) &&
24
+ current.id) {
25
+ names.add(current.id.name);
26
+ }
27
+ for (const key of sourceCode.visitorKeys[current.type] ?? []) {
28
+ const value = current[key];
29
+ const children = Array.isArray(value) ? value : [value];
30
+ for (const child of children) {
31
+ if (child && typeof child === 'object' && 'type' in child) {
32
+ stack.push(child);
33
+ }
34
+ }
35
+ }
36
+ }
37
+ return names;
38
+ }
39
+ /**
40
+ * Reports whether an identifier's declared type names a class declared in this
41
+ * file — the annotation-carried form of a class instance (`user: User` as a
42
+ * parameter or an annotated variable), which the docs promise the same
43
+ * exemption as a `new User()` initializer (#1619).
44
+ */
45
+ function annotationNamesFileClass(identifier, classNames) {
46
+ if (!identifier || identifier.type !== utils_1.AST_NODE_TYPES.Identifier) {
47
+ return false;
48
+ }
49
+ const annotation = identifier.typeAnnotation?.typeAnnotation;
50
+ return (annotation?.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
51
+ annotation.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
52
+ classNames.has(annotation.typeName.name));
53
+ }
54
+ const fileClassNames = new WeakMap();
55
+ function classNamesFor(context) {
56
+ const sourceCode = context.getSourceCode();
57
+ const cached = fileClassNames.get(sourceCode.ast);
58
+ if (cached) {
59
+ return cached;
60
+ }
61
+ const names = collectClassNames(sourceCode);
62
+ fileClassNames.set(sourceCode.ast, names);
63
+ return names;
64
+ }
12
65
  function isClassInstance(node, context) {
13
66
  // Check if node is a MemberExpression
14
67
  if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
@@ -22,12 +75,22 @@ function isClassInstance(node, context) {
22
75
  const variable = object.name;
23
76
  const scope = context.getScope();
24
77
  const ref = scope.references.find((ref) => ref.identifier.name === variable);
25
- if (ref?.resolved?.defs[0]?.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
26
- const init = ref.resolved.defs[0].node.init;
27
- return init?.type === utils_1.AST_NODE_TYPES.NewExpression;
78
+ const def = ref?.resolved?.defs[0];
79
+ if (def?.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
80
+ const init = def.node.init;
81
+ return (init?.type === utils_1.AST_NODE_TYPES.NewExpression ||
82
+ // `const user: User = getUser();` — the annotation, not the
83
+ // initializer, is what marks the value as a class instance.
84
+ annotationNamesFileClass(def.node.id, classNamesFor(context)));
85
+ }
86
+ // `function greet(user: User)` — a parameter typed with a same-file
87
+ // class is a class instance the initializer-based check cannot see.
88
+ if (def?.type === 'Parameter' &&
89
+ annotationNamesFileClass(def.name, classNamesFor(context))) {
90
+ return true;
28
91
  }
29
92
  // Check if the identifier refers to a class (not an instance)
30
- if (ref?.resolved?.defs[0]?.node.type === utils_1.AST_NODE_TYPES.ClassDeclaration) {
93
+ if (def?.node.type === utils_1.AST_NODE_TYPES.ClassDeclaration) {
31
94
  return false;
32
95
  }
33
96
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.80",
3
+ "version": "1.20.81",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,26 @@
1
1
  [
2
+ {
3
+ "version": "1.20.81",
4
+ "date": "2026-08-02T12:22:56.245Z",
5
+ "rules": [
6
+ {
7
+ "name": "no-unused-props",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1620
11
+ ],
12
+ "summary": "resolve the props type from an FC-annotated declarator (closes #1620)"
13
+ },
14
+ {
15
+ "name": "prefer-destructuring-no-class",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 1619
19
+ ],
20
+ "summary": "recognize annotation-carried class instances (closes #1619)"
21
+ }
22
+ ]
23
+ },
2
24
  {
3
25
  "version": "1.20.80",
4
26
  "date": "2026-08-02T11:35:36.730Z",