@cedarjs/eslint-plugin 6.0.0-rc.312 → 6.0.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,3 @@
1
+ import type { TSESLint } from '@typescript-eslint/utils';
2
+ export declare const cellTypeAnnotations: TSESLint.RuleModule<"needsTypeAnnotation" | "beforeQueryParamNeedsType", [], unknown, TSESLint.RuleListener>;
3
+ //# sourceMappingURL=cell-type-annotations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cell-type-annotations.d.ts","sourceRoot":"","sources":["../src/cell-type-annotations.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAY,MAAM,0BAA0B,CAAA;AAwHlE,eAAO,MAAM,mBAAmB,8GAiZ9B,CAAA"}
@@ -0,0 +1,348 @@
1
+ import { basename } from "node:path";
2
+ import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
3
+ const createRule = ESLintUtils.RuleCreator.withoutDocs;
4
+ const LIFECYCLE_EXPORTS = /* @__PURE__ */ new Set(["beforeQuery", "afterQuery", "isEmpty"]);
5
+ const RENDER_PROP_EXPORTS = /* @__PURE__ */ new Set(["Loading", "Failure", "Success"]);
6
+ function getParamPattern(param) {
7
+ return param.type === AST_NODE_TYPES.AssignmentPattern ? param.left : param;
8
+ }
9
+ function paramHasTypeAnnotation(param) {
10
+ const pattern = getParamPattern(param);
11
+ return "typeAnnotation" in pattern && !!pattern.typeAnnotation;
12
+ }
13
+ function getParamsClosingParen(sourceCode, fn) {
14
+ if (fn.params.length > 0) {
15
+ const lastParam = fn.params[fn.params.length - 1];
16
+ return sourceCode.getTokenAfter(lastParam, (token) => token.value === ")");
17
+ }
18
+ return sourceCode.getFirstToken(fn, (token) => token.value === ")");
19
+ }
20
+ function isUnparenthesizedArrowParam(fn, param, sourceCode) {
21
+ if (fn.type !== AST_NODE_TYPES.ArrowFunctionExpression) {
22
+ return false;
23
+ }
24
+ if (fn.params.length !== 1 || fn.params[0] !== param) {
25
+ return false;
26
+ }
27
+ return sourceCode.getTokenBefore(param)?.value !== "(";
28
+ }
29
+ function* insertWrappedAnnotations(fixer, param, paramTypeText, returnTypeText) {
30
+ yield fixer.insertTextBefore(param, "(");
31
+ const paramPart = paramTypeText ? `: ${paramTypeText}` : "";
32
+ const returnPart = returnTypeText ? `: ${returnTypeText}` : "";
33
+ yield fixer.insertTextAfter(param, `${paramPart})${returnPart}`);
34
+ }
35
+ function extractTypedDocumentNodeArgs(typeAnnotation, sourceCode) {
36
+ const type = typeAnnotation?.typeAnnotation;
37
+ if (type?.type !== AST_NODE_TYPES.TSTypeReference) {
38
+ return null;
39
+ }
40
+ if (type.typeName.type !== AST_NODE_TYPES.Identifier || type.typeName.name !== "TypedDocumentNode") {
41
+ return null;
42
+ }
43
+ const args = type.typeArguments?.params;
44
+ if (!args || args.length < 2) {
45
+ return null;
46
+ }
47
+ return {
48
+ data: sourceCode.getText(args[0]),
49
+ variables: sourceCode.getText(args[1])
50
+ };
51
+ }
52
+ function isImportSpecifier(specifier) {
53
+ return specifier.type === AST_NODE_TYPES.ImportSpecifier;
54
+ }
55
+ const cellTypeAnnotations = createRule({
56
+ meta: {
57
+ type: "suggestion",
58
+ docs: {
59
+ description: "Sets the types on a Cell file's lifecycle hooks and render props to the correct @cedarjs/web Cell types"
60
+ },
61
+ messages: {
62
+ needsTypeAnnotation: "The `{{name}}` {{kind}} needs a type annotation of `{{typeName}}` ({{location}}).",
63
+ beforeQueryParamNeedsType: "`beforeQuery`'s first parameter needs a type annotation. Cedar infers the Cell's external props from it, so leaving it untyped degrades type checking at every call site of this Cell."
64
+ },
65
+ fixable: "code",
66
+ schema: []
67
+ },
68
+ defaultOptions: [],
69
+ create(context) {
70
+ const sourceCode = context.sourceCode;
71
+ const filename = basename(context.filename);
72
+ const filenameIndicatesCell = filename.endsWith("Cell.tsx");
73
+ let hasQueryOrFragment = false;
74
+ let hasSuccessExport = false;
75
+ let derivedQueryTypes = null;
76
+ let webTypeImport = null;
77
+ const importedWebTypeNames = /* @__PURE__ */ new Set();
78
+ const checks = [];
79
+ const claimedImportNames = /* @__PURE__ */ new Set();
80
+ function needsImportEdit(importName) {
81
+ if (!importName) {
82
+ return false;
83
+ }
84
+ if (importedWebTypeNames.has(importName)) {
85
+ return false;
86
+ }
87
+ if (claimedImportNames.has(importName)) {
88
+ return false;
89
+ }
90
+ claimedImportNames.add(importName);
91
+ return true;
92
+ }
93
+ function* importEdit(fixer, importName) {
94
+ const specifiers = webTypeImport?.specifiers.filter(isImportSpecifier) ?? [];
95
+ const lastSpecifier = specifiers[specifiers.length - 1];
96
+ if (lastSpecifier) {
97
+ yield fixer.insertTextAfter(lastSpecifier, `, ${importName}`);
98
+ return;
99
+ }
100
+ yield fixer.insertTextBeforeRange(
101
+ [0, 0],
102
+ `import type { ${importName} } from '@cedarjs/web'
103
+ `
104
+ );
105
+ }
106
+ function checkBeforeQuery(fn, reportNode) {
107
+ const variablesType = derivedQueryTypes?.variables ?? "Record<string, unknown>";
108
+ if (!fn.returnType) {
109
+ const typeName = `CellBeforeQueryResult<${variablesType}>`;
110
+ const shouldImport = needsImportEdit("CellBeforeQueryResult");
111
+ context.report({
112
+ node: reportNode,
113
+ messageId: "needsTypeAnnotation",
114
+ data: {
115
+ name: "beforeQuery",
116
+ typeName,
117
+ kind: "function",
118
+ location: "return type"
119
+ },
120
+ *fix(fixer) {
121
+ const [firstParam2] = fn.params;
122
+ if (firstParam2 && isUnparenthesizedArrowParam(fn, firstParam2, sourceCode)) {
123
+ yield* insertWrappedAnnotations(fixer, firstParam2, null, typeName);
124
+ } else {
125
+ const closingParen = getParamsClosingParen(sourceCode, fn);
126
+ if (!closingParen) {
127
+ return;
128
+ }
129
+ yield fixer.insertTextAfter(closingParen, `: ${typeName}`);
130
+ }
131
+ if (shouldImport) {
132
+ yield* importEdit(fixer, "CellBeforeQueryResult");
133
+ }
134
+ }
135
+ });
136
+ }
137
+ const firstParam = fn.params[0];
138
+ if (firstParam && !paramHasTypeAnnotation(firstParam)) {
139
+ context.report({
140
+ node: reportNode,
141
+ messageId: "beforeQueryParamNeedsType"
142
+ });
143
+ }
144
+ }
145
+ function checkAfterQuery(fn, reportNode) {
146
+ const firstParam = fn.params[0];
147
+ const missingParam = !!firstParam && !paramHasTypeAnnotation(firstParam);
148
+ if (!missingParam) {
149
+ return;
150
+ }
151
+ const shouldImport = needsImportEdit("DataObject");
152
+ context.report({
153
+ node: reportNode,
154
+ messageId: "needsTypeAnnotation",
155
+ data: {
156
+ name: "afterQuery",
157
+ typeName: "DataObject",
158
+ kind: "function",
159
+ location: "parameter"
160
+ },
161
+ *fix(fixer) {
162
+ if (firstParam && isUnparenthesizedArrowParam(fn, firstParam, sourceCode)) {
163
+ yield* insertWrappedAnnotations(
164
+ fixer,
165
+ firstParam,
166
+ "DataObject",
167
+ null
168
+ );
169
+ } else if (firstParam) {
170
+ yield fixer.insertTextAfter(
171
+ getParamPattern(firstParam),
172
+ ": DataObject"
173
+ );
174
+ }
175
+ if (shouldImport) {
176
+ yield* importEdit(fixer, "DataObject");
177
+ }
178
+ }
179
+ });
180
+ }
181
+ function checkIsEmpty(fn, reportNode) {
182
+ const [responseParam, optionsParam] = fn.params;
183
+ const missingResponseType = !!responseParam && !paramHasTypeAnnotation(responseParam);
184
+ const missingOptionsType = !!optionsParam && !paramHasTypeAnnotation(optionsParam);
185
+ if (!missingResponseType && !missingOptionsType) {
186
+ return;
187
+ }
188
+ const shouldImport = needsImportEdit("DataObject");
189
+ context.report({
190
+ node: reportNode,
191
+ messageId: "needsTypeAnnotation",
192
+ data: {
193
+ name: "isEmpty",
194
+ typeName: "DataObject",
195
+ kind: "function",
196
+ location: "parameters"
197
+ },
198
+ *fix(fixer) {
199
+ if (responseParam && isUnparenthesizedArrowParam(fn, responseParam, sourceCode)) {
200
+ yield* insertWrappedAnnotations(
201
+ fixer,
202
+ responseParam,
203
+ missingResponseType ? "DataObject" : null,
204
+ null
205
+ );
206
+ } else {
207
+ if (missingResponseType && responseParam) {
208
+ yield fixer.insertTextAfter(
209
+ getParamPattern(responseParam),
210
+ ": DataObject"
211
+ );
212
+ }
213
+ if (missingOptionsType && optionsParam) {
214
+ yield fixer.insertTextAfter(
215
+ getParamPattern(optionsParam),
216
+ ": { isDataEmpty: (data: DataObject) => boolean }"
217
+ );
218
+ }
219
+ }
220
+ if (shouldImport) {
221
+ yield* importEdit(fixer, "DataObject");
222
+ }
223
+ }
224
+ });
225
+ }
226
+ function checkRenderProp(name, fn, reportNode) {
227
+ const [propsParam] = fn.params;
228
+ if (!propsParam || paramHasTypeAnnotation(propsParam)) {
229
+ return;
230
+ }
231
+ const variablesType = derivedQueryTypes?.variables;
232
+ let insertText;
233
+ if (name === "Success") {
234
+ insertText = derivedQueryTypes ? `CellSuccessProps<${derivedQueryTypes.data}, ${derivedQueryTypes.variables}>` : "CellSuccessProps";
235
+ } else if (name === "Failure") {
236
+ insertText = variablesType ? `CellFailureProps<${variablesType}>` : "CellFailureProps";
237
+ } else {
238
+ insertText = variablesType ? `CellLoadingProps<${variablesType}>` : "CellLoadingProps";
239
+ }
240
+ const importName = `Cell${name}Props`;
241
+ const shouldImport = needsImportEdit(importName);
242
+ context.report({
243
+ node: reportNode,
244
+ messageId: "needsTypeAnnotation",
245
+ data: {
246
+ name,
247
+ typeName: insertText,
248
+ kind: "component",
249
+ location: "parameter"
250
+ },
251
+ *fix(fixer) {
252
+ if (isUnparenthesizedArrowParam(fn, propsParam, sourceCode)) {
253
+ yield* insertWrappedAnnotations(fixer, propsParam, insertText, null);
254
+ } else {
255
+ yield fixer.insertTextAfter(
256
+ getParamPattern(propsParam),
257
+ `: ${insertText}`
258
+ );
259
+ }
260
+ if (shouldImport) {
261
+ yield* importEdit(fixer, importName);
262
+ }
263
+ }
264
+ });
265
+ }
266
+ function checkExport(name, fn, reportNode) {
267
+ if (name === "beforeQuery") {
268
+ checkBeforeQuery(fn, reportNode);
269
+ } else if (name === "afterQuery") {
270
+ checkAfterQuery(fn, reportNode);
271
+ } else if (name === "isEmpty") {
272
+ checkIsEmpty(fn, reportNode);
273
+ } else if (RENDER_PROP_EXPORTS.has(name)) {
274
+ checkRenderProp(name, fn, reportNode);
275
+ }
276
+ }
277
+ return {
278
+ ImportDeclaration(node) {
279
+ if (node.source.value !== "@cedarjs/web") {
280
+ return;
281
+ }
282
+ if (node.importKind === "type") {
283
+ webTypeImport = node;
284
+ }
285
+ node.specifiers.filter(isImportSpecifier).forEach((specifier) => {
286
+ if (node.importKind === "type" || specifier.importKind === "type") {
287
+ importedWebTypeNames.add(specifier.local.name);
288
+ }
289
+ });
290
+ },
291
+ ExportNamedDeclaration(node) {
292
+ if (node.declaration?.type === AST_NODE_TYPES.FunctionDeclaration) {
293
+ const fn = node.declaration;
294
+ const idNode = fn.id;
295
+ if (!idNode) {
296
+ return;
297
+ }
298
+ const name = idNode.name;
299
+ if (name === "Success") {
300
+ hasSuccessExport = true;
301
+ }
302
+ if (LIFECYCLE_EXPORTS.has(name) || RENDER_PROP_EXPORTS.has(name)) {
303
+ checks.push(() => checkExport(name, fn, idNode));
304
+ }
305
+ return;
306
+ }
307
+ if (node.declaration?.type !== AST_NODE_TYPES.VariableDeclaration) {
308
+ return;
309
+ }
310
+ node.declaration.declarations.forEach((vd) => {
311
+ if (vd.type !== AST_NODE_TYPES.VariableDeclarator || vd.id.type !== AST_NODE_TYPES.Identifier) {
312
+ return;
313
+ }
314
+ const name = vd.id.name;
315
+ if (name === "QUERY") {
316
+ derivedQueryTypes ||= extractTypedDocumentNodeArgs(
317
+ vd.id.typeAnnotation,
318
+ sourceCode
319
+ );
320
+ hasQueryOrFragment = true;
321
+ return;
322
+ }
323
+ if (name === "FRAGMENT") {
324
+ hasQueryOrFragment = true;
325
+ return;
326
+ }
327
+ if (name === "Success") {
328
+ hasSuccessExport = true;
329
+ }
330
+ if ((LIFECYCLE_EXPORTS.has(name) || RENDER_PROP_EXPORTS.has(name)) && (vd.init?.type === AST_NODE_TYPES.ArrowFunctionExpression || vd.init?.type === AST_NODE_TYPES.FunctionExpression)) {
331
+ const fn = vd.init;
332
+ checks.push(() => checkExport(name, fn, vd.id));
333
+ }
334
+ });
335
+ },
336
+ "Program:exit"() {
337
+ const isCellFile = filenameIndicatesCell && hasQueryOrFragment && hasSuccessExport;
338
+ if (!isCellFile) {
339
+ return;
340
+ }
341
+ checks.forEach((check) => check());
342
+ }
343
+ };
344
+ }
345
+ });
346
+ export {
347
+ cellTypeAnnotations
348
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cedarjs/eslint-plugin",
3
- "version": "6.0.0-rc.312",
3
+ "version": "6.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/cedarjs/cedar.git",
@@ -27,7 +27,7 @@
27
27
  "eslint": "8.57.1"
28
28
  },
29
29
  "devDependencies": {
30
- "@cedarjs/framework-tools": "6.0.0-rc.312",
30
+ "@cedarjs/framework-tools": "6.0.0",
31
31
  "@types/eslint": "8.56.12",
32
32
  "@types/estree": "1.0.9",
33
33
  "@typescript-eslint/parser": "8.60.1",