eslint-config-typed 3.7.1 → 3.8.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.
@@ -0,0 +1,11 @@
1
+ import { type TSESLint } from '@typescript-eslint/utils';
2
+ type Options = readonly [
3
+ Readonly<{
4
+ alwaysCheckReactComponentProps?: boolean;
5
+ directiveKeyword?: string;
6
+ }>?
7
+ ];
8
+ type MessageIds = 'incompleteDestructuring';
9
+ export declare const checkDestructuringCompleteness: TSESLint.RuleModule<MessageIds, Options>;
10
+ export {};
11
+ //# sourceMappingURL=check-destructuring-completeness.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check-destructuring-completeness.d.mts","sourceRoot":"","sources":["../../../../src/plugins/ts-restrictions/rules/check-destructuring-completeness.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AAGlC,KAAK,OAAO,GAAG,SAAS;IACtB,QAAQ,CAAC;QACP,8BAA8B,CAAC,EAAE,OAAO,CAAC;QACzC,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC3B,CAAC,CAAC;CACJ,CAAC;AAEF,KAAK,UAAU,GAAG,yBAAyB,CAAC;AA8D5C,eAAO,MAAM,8BAA8B,EAAE,QAAQ,CAAC,UAAU,CAC9D,UAAU,EACV,OAAO,CAmOR,CAAC"}
@@ -0,0 +1,217 @@
1
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+
3
+ const DEFAULT_DIRECTIVE_KEYWORD = '@check-destructuring-completeness';
4
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
5
+ const getObjectTypeProperties = (type) => {
6
+ try {
7
+ const properties = type.getProperties();
8
+ // Limit to reasonable number of properties to avoid hangs
9
+ if (properties.length > 1000) {
10
+ return [];
11
+ }
12
+ return properties
13
+ .map((prop) => prop.name)
14
+ .filter((name) =>
15
+ // Filter out symbol properties and internal properties
16
+ !name.startsWith('__') &&
17
+ // Only include string property names
18
+ typeof name === 'string' &&
19
+ name.length > 0);
20
+ }
21
+ catch {
22
+ // If there's any error getting properties, return empty array
23
+ return [];
24
+ }
25
+ };
26
+ const isReactComponentFunction = (node) => {
27
+ if (node === undefined || node === null)
28
+ return false;
29
+ // Arrow function component
30
+ if (node.type === AST_NODE_TYPES.ArrowFunctionExpression) {
31
+ const { body } = node;
32
+ if (body.type === AST_NODE_TYPES.BlockStatement) {
33
+ return body.body.some((statement) => {
34
+ if (statement.type !== AST_NODE_TYPES.ReturnStatement)
35
+ return false;
36
+ const { argument } = statement;
37
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
38
+ if (argument === null || argument === undefined)
39
+ return false;
40
+ const argType = argument.type;
41
+ return argType === 'JSXElement' || argType === 'JSXFragment';
42
+ });
43
+ }
44
+ const bodyType = body.type;
45
+ return bodyType === 'JSXElement' || bodyType === 'JSXFragment';
46
+ }
47
+ return false;
48
+ };
49
+ const checkDestructuringCompleteness = {
50
+ meta: {
51
+ type: 'suggestion',
52
+ docs: {
53
+ description: 'Ensure all properties are destructured from an object when explicitly requested',
54
+ },
55
+ schema: [
56
+ {
57
+ type: 'object',
58
+ properties: {
59
+ alwaysCheckReactComponentProps: {
60
+ type: 'boolean',
61
+ description: 'Always check React component props destructuring without directive keyword',
62
+ },
63
+ directiveKeyword: {
64
+ type: 'string',
65
+ description: 'Custom directive keyword to enable checking (default: "@check-destructuring-completeness")',
66
+ },
67
+ },
68
+ additionalProperties: false,
69
+ },
70
+ ],
71
+ messages: {
72
+ incompleteDestructuring: 'Not all properties are destructured. Missing: {{missingProps}}',
73
+ },
74
+ },
75
+ create: (context) => {
76
+ const parserServices = context.sourceCode.parserServices;
77
+ if (parserServices?.program === undefined ||
78
+ parserServices.program === null ||
79
+ parserServices.esTreeNodeToTSNodeMap === undefined) {
80
+ return {};
81
+ }
82
+ const options = context.options[0] ?? {};
83
+ const alwaysCheckReactComponentProps = options.alwaysCheckReactComponentProps ?? true;
84
+ const directiveKeyword = options.directiveKeyword ?? DEFAULT_DIRECTIVE_KEYWORD;
85
+ const typeChecker = parserServices.program.getTypeChecker();
86
+ const esTreeNodeToTSNodeMap = parserServices.esTreeNodeToTSNodeMap;
87
+ const sourceCode = context.sourceCode;
88
+ const hasDirectiveComment = (node) => {
89
+ // Get the parent VariableDeclaration
90
+ const parent = node.parent;
91
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
92
+ if (parent?.type !== AST_NODE_TYPES.VariableDeclaration) {
93
+ return false;
94
+ }
95
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
96
+ const comments = sourceCode.getCommentsBefore(parent);
97
+ return comments.some((comment) => comment.value.includes(directiveKeyword));
98
+ };
99
+ const isReactComponentPropsDestructuring = (node) => {
100
+ if (!alwaysCheckReactComponentProps)
101
+ return false;
102
+ const parent = node.parent;
103
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
104
+ if (parent === undefined)
105
+ return false;
106
+ // Case 1: const { a, b } = props; inside component
107
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
108
+ if (parent.type === AST_NODE_TYPES.VariableDeclaration) {
109
+ const grandParent = parent.parent;
110
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
111
+ if (grandParent === undefined)
112
+ return false;
113
+ // Check if we're inside a BlockStatement of an arrow function component
114
+ if (grandParent.type === AST_NODE_TYPES.BlockStatement) {
115
+ const greatGrandParent = grandParent.parent;
116
+ if (
117
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
118
+ greatGrandParent?.type === AST_NODE_TYPES.ArrowFunctionExpression &&
119
+ node.init?.type === AST_NODE_TYPES.Identifier) {
120
+ const initName =
121
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
122
+ node.init.type === AST_NODE_TYPES.Identifier
123
+ ? node.init.name
124
+ : undefined;
125
+ if (initName !== undefined &&
126
+ greatGrandParent.params.some((param) => param.type === AST_NODE_TYPES.Identifier &&
127
+ param.name === initName)) {
128
+ return isReactComponentFunction(greatGrandParent);
129
+ }
130
+ }
131
+ }
132
+ if (grandParent.type === AST_NODE_TYPES.ArrowFunctionExpression) {
133
+ return isReactComponentFunction(grandParent);
134
+ }
135
+ }
136
+ return false;
137
+ };
138
+ const checkNode = (node) => {
139
+ if (node.id.type !== AST_NODE_TYPES.ObjectPattern)
140
+ return;
141
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
142
+ if (node.init === undefined || node.init === null)
143
+ return;
144
+ const shouldCheck = hasDirectiveComment(node) || isReactComponentPropsDestructuring(node);
145
+ if (!shouldCheck)
146
+ return;
147
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
148
+ const tsNode = esTreeNodeToTSNodeMap.get(node.init);
149
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
150
+ if (tsNode === undefined)
151
+ return;
152
+ const type = typeChecker.getTypeAtLocation(tsNode);
153
+ const objectProps = getObjectTypeProperties(type);
154
+ const destructuredProps = new Set();
155
+ for (const prop of node.id.properties) {
156
+ if (prop.type === AST_NODE_TYPES.Property &&
157
+ prop.key.type === AST_NODE_TYPES.Identifier) {
158
+ // eslint-disable-next-line functional/immutable-data
159
+ destructuredProps.add(prop.key.name);
160
+ }
161
+ }
162
+ const missingProps = objectProps.filter((prop) => !destructuredProps.has(prop));
163
+ if (missingProps.length > 0) {
164
+ context.report({
165
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
166
+ node: node.id,
167
+ messageId: 'incompleteDestructuring',
168
+ data: {
169
+ missingProps: missingProps.join(', '),
170
+ },
171
+ });
172
+ }
173
+ };
174
+ return {
175
+ VariableDeclarator: checkNode,
176
+ ArrowFunctionExpression: (node) => {
177
+ if (!alwaysCheckReactComponentProps)
178
+ return;
179
+ if (!isReactComponentFunction(node))
180
+ return;
181
+ for (const param of node.params) {
182
+ if (param.type === AST_NODE_TYPES.ObjectPattern) {
183
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
184
+ const tsNode = esTreeNodeToTSNodeMap.get(param);
185
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
186
+ if (tsNode === undefined)
187
+ continue;
188
+ const type = typeChecker.getTypeAtLocation(tsNode);
189
+ const objectProps = getObjectTypeProperties(type);
190
+ const destructuredProps = new Set();
191
+ for (const prop of param.properties) {
192
+ if (prop.type === AST_NODE_TYPES.Property &&
193
+ prop.key.type === AST_NODE_TYPES.Identifier) {
194
+ // eslint-disable-next-line functional/immutable-data
195
+ destructuredProps.add(prop.key.name);
196
+ }
197
+ }
198
+ const missingProps = objectProps.filter((prop) => !destructuredProps.has(prop));
199
+ if (missingProps.length > 0) {
200
+ context.report({
201
+ node: param,
202
+ messageId: 'incompleteDestructuring',
203
+ data: {
204
+ missingProps: missingProps.join(', '),
205
+ },
206
+ });
207
+ }
208
+ }
209
+ }
210
+ },
211
+ };
212
+ },
213
+ defaultOptions: [],
214
+ };
215
+
216
+ export { checkDestructuringCompleteness };
217
+ //# sourceMappingURL=check-destructuring-completeness.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check-destructuring-completeness.mjs","sources":["../../../../src/plugins/ts-restrictions/rules/check-destructuring-completeness.mts"],"sourcesContent":[null],"names":[],"mappings":"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"}
@@ -1,4 +1,8 @@
1
1
  export declare const tsRestrictionsRules: {
2
+ readonly 'check-destructuring-completeness': import("@typescript-eslint/utils/ts-eslint").RuleModule<"incompleteDestructuring", readonly [(Readonly<{
3
+ alwaysCheckReactComponentProps?: boolean;
4
+ directiveKeyword?: string;
5
+ }> | undefined)?], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
2
6
  readonly 'no-restricted-cast-name': import("@typescript-eslint/utils/ts-eslint").RuleModule<"restrictedCast", readonly (string | Readonly<{
3
7
  name: string;
4
8
  fixWith?: Readonly<{
@@ -1 +1 @@
1
- {"version":3,"file":"rules.d.mts","sourceRoot":"","sources":["../../../../src/plugins/ts-restrictions/rules/rules.mts"],"names":[],"mappings":"AAIA,eAAO,MAAM,mBAAmB;;;;;;;;;;;;CAGU,CAAC"}
1
+ {"version":3,"file":"rules.d.mts","sourceRoot":"","sources":["../../../../src/plugins/ts-restrictions/rules/rules.mts"],"names":[],"mappings":"AAKA,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;CAIU,CAAC"}
@@ -1,7 +1,9 @@
1
+ import { checkDestructuringCompleteness } from './check-destructuring-completeness.mjs';
1
2
  import { noRestrictedCastName } from './no-restricted-cast-name.mjs';
2
3
  import { noRestrictedSyntax } from './no-restricted-syntax.mjs';
3
4
 
4
5
  const tsRestrictionsRules = {
6
+ 'check-destructuring-completeness': checkDestructuringCompleteness,
5
7
  'no-restricted-cast-name': noRestrictedCastName,
6
8
  'no-restricted-syntax': noRestrictedSyntax,
7
9
  };
@@ -1 +1 @@
1
- {"version":3,"file":"rules.mjs","sources":["../../../../src/plugins/ts-restrictions/rules/rules.mts"],"sourcesContent":[null],"names":[],"mappings":";;;AAIO,MAAM,mBAAmB,GAAG;AACjC,IAAA,yBAAyB,EAAE,oBAAoB;AAC/C,IAAA,sBAAsB,EAAE,kBAAkB;;;;;"}
1
+ {"version":3,"file":"rules.mjs","sources":["../../../../src/plugins/ts-restrictions/rules/rules.mts"],"sourcesContent":[null],"names":[],"mappings":";;;;AAKO,MAAM,mBAAmB,GAAG;AACjC,IAAA,kCAAkC,EAAE,8BAA8B;AAClE,IAAA,yBAAyB,EAAE,oBAAoB;AAC/C,IAAA,sBAAsB,EAAE,kBAAkB;;;;;"}
@@ -1,5 +1,6 @@
1
1
  export declare const eslintTsRestrictionsRules: {
2
2
  readonly 'ts-restrictions/no-restricted-syntax': "off";
3
3
  readonly 'ts-restrictions/no-restricted-cast-name': "off";
4
+ readonly 'ts-restrictions/check-destructuring-completeness': 2 | 1;
4
5
  };
5
6
  //# sourceMappingURL=eslint-ts-restrictions-rules.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"eslint-ts-restrictions-rules.d.mts","sourceRoot":"","sources":["../../src/rules/eslint-ts-restrictions-rules.mts"],"names":[],"mappings":"AAEA,eAAO,MAAM,yBAAyB;;;CAGQ,CAAC"}
1
+ {"version":3,"file":"eslint-ts-restrictions-rules.d.mts","sourceRoot":"","sources":["../../src/rules/eslint-ts-restrictions-rules.mts"],"names":[],"mappings":"AAKA,eAAO,MAAM,yBAAyB;;;;CAKQ,CAAC"}
@@ -1,6 +1,9 @@
1
+ import { withDefaultOption } from '../types/rule-severity-with-default-option.mjs';
2
+
1
3
  const eslintTsRestrictionsRules = {
2
4
  'ts-restrictions/no-restricted-syntax': 'off',
3
5
  'ts-restrictions/no-restricted-cast-name': 'off',
6
+ 'ts-restrictions/check-destructuring-completeness': withDefaultOption('error'),
4
7
  };
5
8
 
6
9
  export { eslintTsRestrictionsRules };
@@ -1 +1 @@
1
- {"version":3,"file":"eslint-ts-restrictions-rules.mjs","sources":["../../src/rules/eslint-ts-restrictions-rules.mts"],"sourcesContent":[null],"names":[],"mappings":"AAEO,MAAM,yBAAyB,GAAG;AACvC,IAAA,sCAAsC,EAAE,KAAK;AAC7C,IAAA,yCAAyC,EAAE,KAAK;;;;;"}
1
+ {"version":3,"file":"eslint-ts-restrictions-rules.mjs","sources":["../../src/rules/eslint-ts-restrictions-rules.mts"],"sourcesContent":[null],"names":[],"mappings":";;AAKO,MAAM,yBAAyB,GAAG;AACvC,IAAA,sCAAsC,EAAE,KAAK;AAC7C,IAAA,yCAAyC,EAAE,KAAK;AAChD,IAAA,kDAAkD,EAChD,iBAAiB,CAAC,OAAO,CAAC;;;;;"}
@@ -1,5 +1,53 @@
1
1
  import { type Linter } from 'eslint';
2
2
  type SpreadOptionsIfIsArray<T extends readonly [Linter.StringSeverity, unknown]> = T[1] extends readonly unknown[] ? readonly [Linter.StringSeverity, ...T[1]] : T;
3
+ /**
4
+ * Ensure all properties are destructured from an object when explicitly
5
+ * requested
6
+ *
7
+ * ```md
8
+ * | key | value |
9
+ * | :--------- | :--------- |
10
+ * | type | suggestion |
11
+ * | deprecated | false |
12
+ * ```
13
+ */
14
+ declare namespace CheckDestructuringCompleteness {
15
+ /**
16
+ * ### schema
17
+ *
18
+ * ```json
19
+ * [
20
+ * {
21
+ * "type": "object",
22
+ * "properties": {
23
+ * "alwaysCheckReactComponentProps": {
24
+ * "type": "boolean",
25
+ * "description": "Always check React component props destructuring without directive keyword"
26
+ * },
27
+ * "directiveKeyword": {
28
+ * "type": "string",
29
+ * "description": "Custom directive keyword to enable checking (default: \"@check-destructuring-completeness\")"
30
+ * }
31
+ * },
32
+ * "additionalProperties": false
33
+ * }
34
+ * ]
35
+ * ```
36
+ */
37
+ type Options = {
38
+ /**
39
+ * Always check React component props destructuring without directive
40
+ * keyword
41
+ */
42
+ readonly alwaysCheckReactComponentProps?: boolean;
43
+ /**
44
+ * Custom directive keyword to enable checking (default:
45
+ * "@check-destructuring-completeness")
46
+ */
47
+ readonly directiveKeyword?: string;
48
+ };
49
+ type RuleEntry = Linter.Severity | SpreadOptionsIfIsArray<readonly [Linter.StringSeverity, Options]> | 'off';
50
+ }
3
51
  /**
4
52
  * Disallow type assertions with specified type names
5
53
  *
@@ -139,10 +187,12 @@ declare namespace NoRestrictedSyntax {
139
187
  type RuleEntry = Linter.Severity | SpreadOptionsIfIsArray<readonly [Linter.StringSeverity, Options]> | 'off';
140
188
  }
141
189
  export type EslintTsRestrictionsRules = {
190
+ readonly 'ts-restrictions/check-destructuring-completeness': CheckDestructuringCompleteness.RuleEntry;
142
191
  readonly 'ts-restrictions/no-restricted-cast-name': NoRestrictedCastName.RuleEntry;
143
192
  readonly 'ts-restrictions/no-restricted-syntax': NoRestrictedSyntax.RuleEntry;
144
193
  };
145
194
  export type EslintTsRestrictionsRulesOption = {
195
+ readonly 'ts-restrictions/check-destructuring-completeness': CheckDestructuringCompleteness.Options;
146
196
  readonly 'ts-restrictions/no-restricted-cast-name': NoRestrictedCastName.Options;
147
197
  readonly 'ts-restrictions/no-restricted-syntax': NoRestrictedSyntax.Options;
148
198
  };
@@ -1 +1 @@
1
- {"version":3,"file":"eslint-ts-restrictions-rules.d.mts","sourceRoot":"","sources":["../../../src/types/rules/eslint-ts-restrictions-rules.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,KAAK,sBAAsB,CACzB,CAAC,SAAS,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,IACjD,CAAC,CAAC,CAAC,CAAC,SAAS,SAAS,OAAO,EAAE,GAC/B,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GACzC,CAAC,CAAC;AAEN;;;;;;;;;;GAUG;AACH,kBAAU,oBAAoB,CAAC;IAC7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4DG;IACH,kBAAkB;IAClB,KAAY,OAAO,GAAG,SAAS,CAC3B,MAAM,GACN;QACE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,OAAO,CAAC,EACb;YACE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;YAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;SACvB,GACD;YACE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;YACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;SACvB,CAAC;KACP,CACJ,EAAE,CAAC;IAEJ,KAAY,SAAS,GACjB,MAAM,CAAC,QAAQ,GACf,sBAAsB,CAAC,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,GACjE,KAAK,CAAC;CACX;AAED;;;;;;;;;;;;GAYG;AACH,kBAAU,kBAAkB,CAAC;IAC3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8BG;IACH,kBAAkB;IAClB,KAAY,OAAO,GAAG,SAAS,CAC3B,MAAM,GACN;QACE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;KAC3B,CACJ,EAAE,CAAC;IAEJ,KAAY,SAAS,GACjB,MAAM,CAAC,QAAQ,GACf,sBAAsB,CAAC,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,GACjE,KAAK,CAAC;CACX;AAED,MAAM,MAAM,yBAAyB,GAAG;IACtC,QAAQ,CAAC,yCAAyC,EAAE,oBAAoB,CAAC,SAAS,CAAC;IACnF,QAAQ,CAAC,sCAAsC,EAAE,kBAAkB,CAAC,SAAS,CAAC;CAC/E,CAAC;AAEF,MAAM,MAAM,+BAA+B,GAAG;IAC5C,QAAQ,CAAC,yCAAyC,EAAE,oBAAoB,CAAC,OAAO,CAAC;IACjF,QAAQ,CAAC,sCAAsC,EAAE,kBAAkB,CAAC,OAAO,CAAC;CAC7E,CAAC"}
1
+ {"version":3,"file":"eslint-ts-restrictions-rules.d.mts","sourceRoot":"","sources":["../../../src/types/rules/eslint-ts-restrictions-rules.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,KAAK,sBAAsB,CACzB,CAAC,SAAS,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,IACjD,CAAC,CAAC,CAAC,CAAC,SAAS,SAAS,OAAO,EAAE,GAC/B,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GACzC,CAAC,CAAC;AAEN;;;;;;;;;;GAUG;AACH,kBAAU,8BAA8B,CAAC;IACvC;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,KAAY,OAAO,GAAG;QACpB;;;WAGG;QACH,QAAQ,CAAC,8BAA8B,CAAC,EAAE,OAAO,CAAC;QAClD;;;WAGG;QACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;KACpC,CAAC;IAEF,KAAY,SAAS,GACjB,MAAM,CAAC,QAAQ,GACf,sBAAsB,CAAC,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,GACjE,KAAK,CAAC;CACX;AAED;;;;;;;;;;GAUG;AACH,kBAAU,oBAAoB,CAAC;IAC7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4DG;IACH,kBAAkB;IAClB,KAAY,OAAO,GAAG,SAAS,CAC3B,MAAM,GACN;QACE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,OAAO,CAAC,EACb;YACE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;YAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;SACvB,GACD;YACE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;YACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;SACvB,CAAC;KACP,CACJ,EAAE,CAAC;IAEJ,KAAY,SAAS,GACjB,MAAM,CAAC,QAAQ,GACf,sBAAsB,CAAC,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,GACjE,KAAK,CAAC;CACX;AAED;;;;;;;;;;;;GAYG;AACH,kBAAU,kBAAkB,CAAC;IAC3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8BG;IACH,kBAAkB;IAClB,KAAY,OAAO,GAAG,SAAS,CAC3B,MAAM,GACN;QACE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;KAC3B,CACJ,EAAE,CAAC;IAEJ,KAAY,SAAS,GACjB,MAAM,CAAC,QAAQ,GACf,sBAAsB,CAAC,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,GACjE,KAAK,CAAC;CACX;AAED,MAAM,MAAM,yBAAyB,GAAG;IACtC,QAAQ,CAAC,kDAAkD,EAAE,8BAA8B,CAAC,SAAS,CAAC;IACtG,QAAQ,CAAC,yCAAyC,EAAE,oBAAoB,CAAC,SAAS,CAAC;IACnF,QAAQ,CAAC,sCAAsC,EAAE,kBAAkB,CAAC,SAAS,CAAC;CAC/E,CAAC;AAEF,MAAM,MAAM,+BAA+B,GAAG;IAC5C,QAAQ,CAAC,kDAAkD,EAAE,8BAA8B,CAAC,OAAO,CAAC;IACpG,QAAQ,CAAC,yCAAyC,EAAE,oBAAoB,CAAC,OAAO,CAAC;IACjF,QAAQ,CAAC,sCAAsC,EAAE,kBAAkB,CAAC,OAAO,CAAC;CAC7E,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-config-typed",
3
- "version": "3.7.1",
3
+ "version": "3.8.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "typescript"
@@ -0,0 +1,306 @@
1
+ import {
2
+ AST_NODE_TYPES,
3
+ type TSESLint,
4
+ type TSESTree,
5
+ } from '@typescript-eslint/utils';
6
+ import type * as ts from 'typescript';
7
+
8
+ type Options = readonly [
9
+ Readonly<{
10
+ alwaysCheckReactComponentProps?: boolean;
11
+ directiveKeyword?: string;
12
+ }>?,
13
+ ];
14
+
15
+ type MessageIds = 'incompleteDestructuring';
16
+
17
+ const DEFAULT_DIRECTIVE_KEYWORD = '@check-destructuring-completeness';
18
+
19
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
20
+ const getObjectTypeProperties = (type: ts.Type): readonly string[] => {
21
+ try {
22
+ const properties = type.getProperties();
23
+
24
+ // Limit to reasonable number of properties to avoid hangs
25
+ if (properties.length > 1000) {
26
+ return [];
27
+ }
28
+
29
+ return properties
30
+ .map((prop) => prop.name)
31
+ .filter(
32
+ (name) =>
33
+ // Filter out symbol properties and internal properties
34
+ !name.startsWith('__') &&
35
+ // Only include string property names
36
+ typeof name === 'string' &&
37
+ name.length > 0,
38
+ );
39
+ } catch {
40
+ // If there's any error getting properties, return empty array
41
+ return [];
42
+ }
43
+ };
44
+
45
+ const isReactComponentFunction = (
46
+ node: DeepReadonly<TSESTree.Node> | undefined | null,
47
+ ): boolean => {
48
+ if (node === undefined || node === null) return false;
49
+
50
+ // Arrow function component
51
+ if (node.type === AST_NODE_TYPES.ArrowFunctionExpression) {
52
+ const { body } = node;
53
+
54
+ if (body.type === AST_NODE_TYPES.BlockStatement) {
55
+ return body.body.some((statement) => {
56
+ if (statement.type !== AST_NODE_TYPES.ReturnStatement) return false;
57
+
58
+ const { argument } = statement;
59
+
60
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
61
+ if (argument === null || argument === undefined) return false;
62
+
63
+ const argType = (argument as { type?: string }).type;
64
+
65
+ return argType === 'JSXElement' || argType === 'JSXFragment';
66
+ });
67
+ }
68
+
69
+ const bodyType = (body as { type?: string }).type;
70
+
71
+ return bodyType === 'JSXElement' || bodyType === 'JSXFragment';
72
+ }
73
+
74
+ return false;
75
+ };
76
+
77
+ export const checkDestructuringCompleteness: TSESLint.RuleModule<
78
+ MessageIds,
79
+ Options
80
+ > = {
81
+ meta: {
82
+ type: 'suggestion',
83
+ docs: {
84
+ description:
85
+ 'Ensure all properties are destructured from an object when explicitly requested',
86
+ },
87
+ schema: [
88
+ {
89
+ type: 'object',
90
+ properties: {
91
+ alwaysCheckReactComponentProps: {
92
+ type: 'boolean',
93
+ description:
94
+ 'Always check React component props destructuring without directive keyword',
95
+ },
96
+ directiveKeyword: {
97
+ type: 'string',
98
+ description:
99
+ 'Custom directive keyword to enable checking (default: "@check-destructuring-completeness")',
100
+ },
101
+ },
102
+ additionalProperties: false,
103
+ },
104
+ ],
105
+ messages: {
106
+ incompleteDestructuring:
107
+ 'Not all properties are destructured. Missing: {{missingProps}}',
108
+ },
109
+ },
110
+
111
+ create: (context) => {
112
+ const parserServices = context.sourceCode.parserServices;
113
+
114
+ if (
115
+ parserServices?.program === undefined ||
116
+ parserServices.program === null ||
117
+ parserServices.esTreeNodeToTSNodeMap === undefined
118
+ ) {
119
+ return {};
120
+ }
121
+
122
+ const options = context.options[0] ?? {};
123
+
124
+ const alwaysCheckReactComponentProps =
125
+ options.alwaysCheckReactComponentProps ?? true;
126
+
127
+ const directiveKeyword =
128
+ options.directiveKeyword ?? DEFAULT_DIRECTIVE_KEYWORD;
129
+
130
+ const typeChecker = parserServices.program.getTypeChecker();
131
+ const esTreeNodeToTSNodeMap = parserServices.esTreeNodeToTSNodeMap;
132
+ const sourceCode = context.sourceCode;
133
+
134
+ const hasDirectiveComment = (
135
+ node: DeepReadonly<TSESTree.VariableDeclarator>,
136
+ ): boolean => {
137
+ // Get the parent VariableDeclaration
138
+ const parent = node.parent;
139
+
140
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
141
+ if (parent?.type !== AST_NODE_TYPES.VariableDeclaration) {
142
+ return false;
143
+ }
144
+
145
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
146
+ const comments = sourceCode.getCommentsBefore(parent as never);
147
+
148
+ return comments.some((comment) =>
149
+ comment.value.includes(directiveKeyword),
150
+ );
151
+ };
152
+
153
+ const isReactComponentPropsDestructuring = (
154
+ node: DeepReadonly<TSESTree.VariableDeclarator>,
155
+ ): boolean => {
156
+ if (!alwaysCheckReactComponentProps) return false;
157
+
158
+ const parent = node.parent;
159
+
160
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
161
+ if (parent === undefined) return false;
162
+
163
+ // Case 1: const { a, b } = props; inside component
164
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
165
+ if (parent.type === AST_NODE_TYPES.VariableDeclaration) {
166
+ const grandParent = parent.parent;
167
+
168
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
169
+ if (grandParent === undefined) return false;
170
+
171
+ // Check if we're inside a BlockStatement of an arrow function component
172
+ if (grandParent.type === AST_NODE_TYPES.BlockStatement) {
173
+ const greatGrandParent = grandParent.parent;
174
+
175
+ if (
176
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
177
+ greatGrandParent?.type === AST_NODE_TYPES.ArrowFunctionExpression &&
178
+ node.init?.type === AST_NODE_TYPES.Identifier
179
+ ) {
180
+ const initName =
181
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
182
+ node.init.type === AST_NODE_TYPES.Identifier
183
+ ? node.init.name
184
+ : undefined;
185
+
186
+ if (
187
+ initName !== undefined &&
188
+ greatGrandParent.params.some(
189
+ (param) =>
190
+ param.type === AST_NODE_TYPES.Identifier &&
191
+ param.name === initName,
192
+ )
193
+ ) {
194
+ return isReactComponentFunction(greatGrandParent);
195
+ }
196
+ }
197
+ }
198
+
199
+ if (grandParent.type === AST_NODE_TYPES.ArrowFunctionExpression) {
200
+ return isReactComponentFunction(grandParent);
201
+ }
202
+ }
203
+
204
+ return false;
205
+ };
206
+
207
+ const checkNode = (
208
+ node: DeepReadonly<TSESTree.VariableDeclarator>,
209
+ ): void => {
210
+ if (node.id.type !== AST_NODE_TYPES.ObjectPattern) return;
211
+
212
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
213
+ if (node.init === undefined || node.init === null) return;
214
+
215
+ const shouldCheck =
216
+ hasDirectiveComment(node) || isReactComponentPropsDestructuring(node);
217
+
218
+ if (!shouldCheck) return;
219
+
220
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
221
+ const tsNode = esTreeNodeToTSNodeMap.get(node.init as never);
222
+
223
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
224
+ if (tsNode === undefined) return;
225
+
226
+ const type = typeChecker.getTypeAtLocation(tsNode);
227
+
228
+ const objectProps = getObjectTypeProperties(type);
229
+ const destructuredProps = new Set<string>();
230
+
231
+ for (const prop of node.id.properties) {
232
+ if (
233
+ prop.type === AST_NODE_TYPES.Property &&
234
+ prop.key.type === AST_NODE_TYPES.Identifier
235
+ ) {
236
+ // eslint-disable-next-line functional/immutable-data
237
+ destructuredProps.add(prop.key.name);
238
+ }
239
+ }
240
+
241
+ const missingProps = objectProps.filter(
242
+ (prop) => !destructuredProps.has(prop),
243
+ );
244
+
245
+ if (missingProps.length > 0) {
246
+ context.report({
247
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
248
+ node: node.id as never,
249
+ messageId: 'incompleteDestructuring',
250
+ data: {
251
+ missingProps: missingProps.join(', '),
252
+ },
253
+ });
254
+ }
255
+ };
256
+
257
+ return {
258
+ VariableDeclarator: checkNode,
259
+ ArrowFunctionExpression: (node) => {
260
+ if (!alwaysCheckReactComponentProps) return;
261
+
262
+ if (!isReactComponentFunction(node)) return;
263
+
264
+ for (const param of node.params) {
265
+ if (param.type === AST_NODE_TYPES.ObjectPattern) {
266
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
267
+ const tsNode = esTreeNodeToTSNodeMap.get(param as never);
268
+
269
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
270
+ if (tsNode === undefined) continue;
271
+
272
+ const type = typeChecker.getTypeAtLocation(tsNode);
273
+
274
+ const objectProps = getObjectTypeProperties(type);
275
+ const destructuredProps = new Set<string>();
276
+
277
+ for (const prop of param.properties) {
278
+ if (
279
+ prop.type === AST_NODE_TYPES.Property &&
280
+ prop.key.type === AST_NODE_TYPES.Identifier
281
+ ) {
282
+ // eslint-disable-next-line functional/immutable-data
283
+ destructuredProps.add(prop.key.name);
284
+ }
285
+ }
286
+
287
+ const missingProps = objectProps.filter(
288
+ (prop) => !destructuredProps.has(prop),
289
+ );
290
+
291
+ if (missingProps.length > 0) {
292
+ context.report({
293
+ node: param,
294
+ messageId: 'incompleteDestructuring',
295
+ data: {
296
+ missingProps: missingProps.join(', '),
297
+ },
298
+ });
299
+ }
300
+ }
301
+ }
302
+ },
303
+ };
304
+ },
305
+ defaultOptions: [],
306
+ };
@@ -0,0 +1,158 @@
1
+ import parser from '@typescript-eslint/parser';
2
+ import { RuleTester } from '@typescript-eslint/rule-tester';
3
+ import dedent from 'dedent';
4
+ import { checkDestructuringCompleteness } from './check-destructuring-completeness.mjs';
5
+
6
+ const tester = new RuleTester({
7
+ languageOptions: {
8
+ parser,
9
+ parserOptions: {
10
+ ecmaVersion: 2020,
11
+ sourceType: 'module',
12
+ ecmaFeatures: {
13
+ jsx: true,
14
+ },
15
+ projectService: {
16
+ allowDefaultProject: ['*.ts*'],
17
+ },
18
+ tsconfigRootDir: `${import.meta.dirname}/../../../..`,
19
+ },
20
+ },
21
+ });
22
+
23
+ describe('check-destructuring-completeness', () => {
24
+ tester.run(
25
+ 'check-destructuring-completeness',
26
+ checkDestructuringCompleteness,
27
+ {
28
+ valid: [
29
+ {
30
+ name: 'ignore incomplete destructuring without directive comment',
31
+ code: dedent`
32
+ const obj = { a: 1, b: 2, c: 3 };
33
+ const { a } = obj;
34
+ `,
35
+ },
36
+ {
37
+ name: 'validates destructuring with default directive - complete',
38
+ code: dedent`
39
+ const obj = { a: 1, b: 2, c: 3 };
40
+ // @check-destructuring-completeness
41
+ const { a, b, c } = obj;
42
+ `,
43
+ },
44
+ {
45
+ name: 'validates destructuring with unknown directive',
46
+ code: dedent`
47
+ const obj: { a: number; b: string; c: boolean } = { a: 1, b: 'hello', c: true };
48
+ // @unknown-directive
49
+ const { a, b } = obj;
50
+ `,
51
+ },
52
+ {
53
+ name: 'works with custom directive keyword - custom check',
54
+ code: dedent`
55
+ const obj: { a: number; b: string } = { a: 1, b: 'hello' };
56
+ // @custom-check
57
+ const { a, b } = obj;
58
+ `,
59
+ options: [{ directiveKeyword: '@custom-check' }],
60
+ },
61
+ {
62
+ name: 'custom directive ignores default directive',
63
+ code: dedent`
64
+ const obj = { a: 1, b: 2, c: 3 };
65
+ // @check-destructuring-completeness
66
+ const { a, b } = obj;
67
+ `,
68
+ options: [{ directiveKeyword: '@custom-check' }],
69
+ },
70
+ {
71
+ name: 'custom directive ignores unknown directive',
72
+ code: dedent`
73
+ const obj: { a: number; b: string; c: boolean } = { a: 1, b: 'hello', c: true };
74
+ // @unknown-directive
75
+ const { a, b } = obj;
76
+ `,
77
+ options: [{ directiveKeyword: '@custom-check' }],
78
+ },
79
+ {
80
+ name: 'checks React component props - parameter destructuring',
81
+ code: dedent`
82
+ type Props = { a: number; b: string };
83
+ const MyComponent = ({ a, b }: Props) => <div>{a}{b}</div>;
84
+ `,
85
+ },
86
+ {
87
+ name: 'checks React component props - internal destructuring',
88
+ code: dedent`
89
+ type Props = { a: number; b: string };
90
+ const MyComponent = (props: Props) => {
91
+ const { a, b } = props;
92
+ return <div>{a}{b}</div>;
93
+ };
94
+ `,
95
+ },
96
+ {
97
+ name: 'does not check React component props when disabled - parameter',
98
+ code: dedent`
99
+ type Props = { a: number; b: string; c: boolean };
100
+ const MyComponent = ({ a, b }: Props) => <div>{a}{b}</div>;
101
+ `,
102
+ options: [{ alwaysCheckReactComponentProps: false }],
103
+ },
104
+ {
105
+ name: 'does not check React component props when disabled - internal',
106
+ code: dedent`
107
+ type Props = { a: number; b: string; c: boolean };
108
+ const MyComponent = (props: Props) => {
109
+ const { a, b } = props;
110
+ return <div>{a}{b}</div>;
111
+ };
112
+ `,
113
+ options: [{ alwaysCheckReactComponentProps: false }],
114
+ },
115
+ ],
116
+ invalid: [
117
+ {
118
+ name: 'validates destructuring with default directive - incomplete',
119
+ code: dedent`
120
+ const obj: { a: number; b: string; c: boolean } = { a: 1, b: 'hello', c: true };
121
+ // @check-destructuring-completeness
122
+ const { a, b } = obj;
123
+ `,
124
+ errors: [{ messageId: 'incompleteDestructuring' }],
125
+ },
126
+ {
127
+ name: 'custom directive catches incomplete',
128
+ code: dedent`
129
+ const obj: { a: number; b: string } = { a: 1, b: 'hello' };
130
+ // @custom-check
131
+ const { a } = obj;
132
+ `,
133
+ options: [{ directiveKeyword: '@custom-check' }],
134
+ errors: [{ messageId: 'incompleteDestructuring' }],
135
+ },
136
+ {
137
+ name: 'checks React component props - parameter destructuring incomplete',
138
+ code: dedent`
139
+ type Props = { a: number; b: string; c: boolean };
140
+ const MyComponent = ({ a, b }: Props) => <div>{a}{b}</div>;
141
+ `,
142
+ errors: [{ messageId: 'incompleteDestructuring' }],
143
+ },
144
+ {
145
+ name: 'checks React component props - internal destructuring incomplete',
146
+ code: dedent`
147
+ type Props = { a: number; b: string; c: boolean };
148
+ const MyComponent = (props: Props) => {
149
+ const { a, b } = props;
150
+ return <div>{a}{b}</div>;
151
+ };
152
+ `,
153
+ errors: [{ messageId: 'incompleteDestructuring' }],
154
+ },
155
+ ],
156
+ },
157
+ );
158
+ }, 20000);
@@ -1,8 +1,10 @@
1
1
  import { type ESLintPlugin } from '../../../types/index.mjs';
2
+ import { checkDestructuringCompleteness } from './check-destructuring-completeness.mjs';
2
3
  import { noRestrictedCastName } from './no-restricted-cast-name.mjs';
3
4
  import { noRestrictedSyntax } from './no-restricted-syntax.mjs';
4
5
 
5
6
  export const tsRestrictionsRules = {
7
+ 'check-destructuring-completeness': checkDestructuringCompleteness,
6
8
  'no-restricted-cast-name': noRestrictedCastName,
7
9
  'no-restricted-syntax': noRestrictedSyntax,
8
10
  } as const satisfies ESLintPlugin['rules'];
@@ -1,6 +1,11 @@
1
- import { type EslintTsRestrictionsRules } from '../types/index.mjs';
1
+ import {
2
+ type EslintTsRestrictionsRules,
3
+ withDefaultOption,
4
+ } from '../types/index.mjs';
2
5
 
3
6
  export const eslintTsRestrictionsRules = {
4
7
  'ts-restrictions/no-restricted-syntax': 'off',
5
8
  'ts-restrictions/no-restricted-cast-name': 'off',
9
+ 'ts-restrictions/check-destructuring-completeness':
10
+ withDefaultOption('error'),
6
11
  } as const satisfies EslintTsRestrictionsRules;
@@ -7,6 +7,59 @@ type SpreadOptionsIfIsArray<
7
7
  ? readonly [Linter.StringSeverity, ...T[1]]
8
8
  : T;
9
9
 
10
+ /**
11
+ * Ensure all properties are destructured from an object when explicitly
12
+ * requested
13
+ *
14
+ * ```md
15
+ * | key | value |
16
+ * | :--------- | :--------- |
17
+ * | type | suggestion |
18
+ * | deprecated | false |
19
+ * ```
20
+ */
21
+ namespace CheckDestructuringCompleteness {
22
+ /**
23
+ * ### schema
24
+ *
25
+ * ```json
26
+ * [
27
+ * {
28
+ * "type": "object",
29
+ * "properties": {
30
+ * "alwaysCheckReactComponentProps": {
31
+ * "type": "boolean",
32
+ * "description": "Always check React component props destructuring without directive keyword"
33
+ * },
34
+ * "directiveKeyword": {
35
+ * "type": "string",
36
+ * "description": "Custom directive keyword to enable checking (default: \"@check-destructuring-completeness\")"
37
+ * }
38
+ * },
39
+ * "additionalProperties": false
40
+ * }
41
+ * ]
42
+ * ```
43
+ */
44
+ export type Options = {
45
+ /**
46
+ * Always check React component props destructuring without directive
47
+ * keyword
48
+ */
49
+ readonly alwaysCheckReactComponentProps?: boolean;
50
+ /**
51
+ * Custom directive keyword to enable checking (default:
52
+ * "@check-destructuring-completeness")
53
+ */
54
+ readonly directiveKeyword?: string;
55
+ };
56
+
57
+ export type RuleEntry =
58
+ | Linter.Severity
59
+ | SpreadOptionsIfIsArray<readonly [Linter.StringSeverity, Options]>
60
+ | 'off';
61
+ }
62
+
10
63
  /**
11
64
  * Disallow type assertions with specified type names
12
65
  *
@@ -164,11 +217,13 @@ namespace NoRestrictedSyntax {
164
217
  }
165
218
 
166
219
  export type EslintTsRestrictionsRules = {
220
+ readonly 'ts-restrictions/check-destructuring-completeness': CheckDestructuringCompleteness.RuleEntry;
167
221
  readonly 'ts-restrictions/no-restricted-cast-name': NoRestrictedCastName.RuleEntry;
168
222
  readonly 'ts-restrictions/no-restricted-syntax': NoRestrictedSyntax.RuleEntry;
169
223
  };
170
224
 
171
225
  export type EslintTsRestrictionsRulesOption = {
226
+ readonly 'ts-restrictions/check-destructuring-completeness': CheckDestructuringCompleteness.Options;
172
227
  readonly 'ts-restrictions/no-restricted-cast-name': NoRestrictedCastName.Options;
173
228
  readonly 'ts-restrictions/no-restricted-syntax': NoRestrictedSyntax.Options;
174
229
  };