@zukhruf/eslint 0.3.14 → 0.3.16

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/README.md CHANGED
@@ -56,7 +56,7 @@ Put framework presets (for example Nx's `flat/react`) **before** the zukhruf con
56
56
 
57
57
  `base` turns on typed lint (`parserOptions.projectService`). Each linted TypeScript file must be in a tsconfig.
58
58
 
59
- `zukhruf/no-promise-field` reads each class field's type, so it also sees a promise behind an alias, in a type argument (`Map<string, Promise<X>>`), or inferred from the initializer. A field that holds a function returning a promise is fine: that is a memoized function. To wait until something has happened, use a state object or a one-shot latch.
59
+ `zukhruf/no-promise-field` reads each class field's type, so it also sees a promise behind an alias, in a type argument (`Map<string, Promise<X>>`, `Record<string, Promise<X>>`), in a union or an intersection, in a `PromiseWithResolvers`, or inferred from the initializer. It also looks inside a record that the project declares, such as `Map<string, { key: string; answer: Promise<boolean> }>`, up to four properties deep. It stops at a class, because the rule checks the fields of that class on their own, and at a library's type, because its promises belong to the library. A field that holds a function returning a promise is fine: that is a memoized function. A record of functions that settle a promise, `{ resolve, reject }`, is fine too: the caller keeps the promise. To wait until something has happened, use a state object or a one-shot latch. See [ADR 0006](./docs/adr/0006-no-promise-field-stops-at-the-class-boundary.md).
60
60
 
61
61
  `zukhruf/no-phase-flag` reads each class field's type too. It reports a field that is a boolean, or that can be `undefined`, when code outside the constructor sets it. A callback that the constructor creates runs later, so its assignment counts as outside. Such a field records a phase, and every reader must know which phase holds. Push the fact as an event, wait on a one-shot latch, keep the phases in state objects, or ask the platform for the fact.
62
62
 
@@ -1 +1 @@
1
- {"version":3,"file":"no-promise-field.d.ts","sourceRoot":"","sources":["../../src/async-values/no-promise-field.ts"],"names":[],"mappings":";AAqFA,wBAAgC"}
1
+ {"version":3,"file":"no-promise-field.d.ts","sourceRoot":"","sources":["../../src/async-values/no-promise-field.ts"],"names":[],"mappings":";AAsLA,wBAAgC"}
@@ -1,22 +1,89 @@
1
1
  import { ESLintUtils } from '@typescript-eslint/utils';
2
- import { isThenableType, isTypeReference, unionConstituents, } from 'ts-api-utils';
2
+ import { intersectionConstituents, isSymbolFlagSet, isThenableType, isTypeReference, unionConstituents, } from 'ts-api-utils';
3
+ import * as ts from 'typescript';
3
4
  import { eslintRule } from "../authoring/rule-module.js";
5
+ /** A file of TypeScript's lib, or of a package under node_modules. */
6
+ const isLibraryFile = (program, file) => program.isSourceFileDefaultLibrary(file) ||
7
+ program.isSourceFileFromExternalLibrary(file);
8
+ /**
9
+ * The result of `Promise.withResolvers()`, from the lib or a polyfill. It keeps
10
+ * its promise in a property, so a check of the type itself misses it.
11
+ */
12
+ function isPromiseWithResolvers({ program }, type) {
13
+ const symbol = type.getSymbol();
14
+ return (symbol?.getName() === 'PromiseWithResolvers' &&
15
+ symbol
16
+ .getDeclarations()
17
+ ?.some((declaration) => isLibraryFile(program, declaration.getSourceFile())) === true);
18
+ }
19
+ /**
20
+ * The type arguments of a generic type (`Map<K, V>`) and of a type alias
21
+ * (`Record<K, V>`, `Partial<T>`), which TypeScript keeps apart.
22
+ */
23
+ function typeArguments({ checker }, type) {
24
+ return [
25
+ ...(isTypeReference(type) ? checker.getTypeArguments(type) : []),
26
+ ...(type.aliasTypeArguments ?? []),
27
+ ];
28
+ }
29
+ /** How many properties deep the walk follows records, so a recursive generic record ends. */
30
+ const RECORD_DEPTH = 4;
31
+ /**
32
+ * A shape of data that the project declares. A class is not one: the rule
33
+ * checks the fields of each class itself. A library's type is not one either:
34
+ * its promises, such as a stream writer's `closed`, belong to the library. A
35
+ * function type is one, but it has no properties, so nothing in it is found.
36
+ */
37
+ function isProjectRecord({ program }, type) {
38
+ const symbol = type.getSymbol();
39
+ if (!symbol || isSymbolFlagSet(symbol, ts.SymbolFlags.Class))
40
+ return false;
41
+ const declarations = symbol.getDeclarations();
42
+ return (declarations !== undefined &&
43
+ declarations.length > 0 &&
44
+ declarations.every((declaration) => !isLibraryFile(program, declaration.getSourceFile())));
45
+ }
46
+ /**
47
+ * What a record holds: its properties and its index signatures. A method is a
48
+ * function type, so the walk finds no promise in it.
49
+ */
50
+ function recordMembers({ checker }, type) {
51
+ return [
52
+ ...checker
53
+ .getPropertiesOfType(type)
54
+ .map((property) => checker.getTypeOfSymbol(property)),
55
+ ...checker.getIndexInfosOfType(type).map((info) => info.type),
56
+ ];
57
+ }
4
58
  /**
5
59
  * Whether `type` is a promise, or carries one as a type argument
6
- * (`Map<string, Promise<X>>`, `Promise<X>[]`). A function type is not entered:
7
- * a field that holds `() => Promise<X>` holds the memoized function this rule
8
- * asks for.
60
+ * (`Map<string, Promise<X>>`, `Promise<X>[]`) or as a member of a record the
61
+ * project declares, in any member of a union or an intersection. A function
62
+ * type is not entered: a field that holds `() => Promise<X>` holds the
63
+ * memoized function this rule asks for.
64
+ *
65
+ * `path` holds only the types on the way to this one: a type that one branch
66
+ * met at the depth limit must still be walked when another branch meets it
67
+ * higher up.
9
68
  */
10
- function holdsPromise(checker, node, type, seen = new Set()) {
11
- if (seen.has(type))
69
+ function holdsPromise(walk, type, depth = 0, path = new Set()) {
70
+ if (path.has(type))
12
71
  return false;
13
- seen.add(type);
14
- if (isThenableType(checker, node, type))
15
- return true;
16
- return unionConstituents(type).some((part) => isTypeReference(part) &&
17
- checker
18
- .getTypeArguments(part)
19
- .some((argument) => holdsPromise(checker, node, argument, seen)));
72
+ path.add(type);
73
+ try {
74
+ if (isThenableType(walk.checker, walk.node, type))
75
+ return true;
76
+ return unionConstituents(type)
77
+ .flatMap((part) => intersectionConstituents(part))
78
+ .some((part) => isPromiseWithResolvers(walk, part) ||
79
+ typeArguments(walk, part).some((argument) => holdsPromise(walk, argument, depth, path)) ||
80
+ (depth < RECORD_DEPTH &&
81
+ isProjectRecord(walk, part) &&
82
+ recordMembers(walk, part).some((member) => holdsPromise(walk, member, depth + 1, path))));
83
+ }
84
+ finally {
85
+ path.delete(type);
86
+ }
20
87
  }
21
88
  /** The node that names a field: its key, or a parameter property's binding. */
22
89
  function fieldName(node) {
@@ -41,12 +108,17 @@ const rule = ESLintUtils.RuleCreator.withoutDocs({
41
108
  create(context) {
42
109
  // Throws the standard "requires type information" error without typed lint.
43
110
  const services = ESLintUtils.getParserServices(context);
44
- const checker = services.program.getTypeChecker();
111
+ const { program } = services;
112
+ const checker = program.getTypeChecker();
45
113
  const check = (node) => {
46
114
  const name = fieldName(node);
47
115
  const type = services.getTypeAtLocation(name);
48
- const tsNode = services.esTreeNodeToTSNodeMap.get(name);
49
- if (holdsPromise(checker, tsNode, type)) {
116
+ const walk = {
117
+ checker,
118
+ program,
119
+ node: services.esTreeNodeToTSNodeMap.get(name),
120
+ };
121
+ if (holdsPromise(walk, type)) {
50
122
  context.report({ node, messageId: 'promiseField' });
51
123
  }
52
124
  };
@@ -1 +1 @@
1
- {"version":3,"file":"no-promise-field.js","sourceRoot":"","sources":["../../src/async-values/no-promise-field.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AACtE,OAAO,EACL,cAAc,EACd,eAAe,EACf,iBAAiB,GAClB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAEzD;;;;;GAKG;AACH,SAAS,YAAY,CACnB,OAAuB,EACvB,IAAa,EACb,IAAa,EACb,OAAO,IAAI,GAAG,EAAW;IAEzB,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACjC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACf,IAAI,cAAc,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACrD,OAAO,iBAAiB,CAAC,IAAI,CAAC,CAAC,IAAI,CACjC,CAAC,IAAI,EAAE,EAAE,CACP,eAAe,CAAC,IAAI,CAAC;QACrB,OAAO;aACJ,gBAAgB,CAAC,IAAI,CAAC;aACtB,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,CACrE,CAAC;AACJ,CAAC;AAED,+EAA+E;AAC/E,SAAS,SAAS,CAChB,IAKgC;IAEhC,IAAI,IAAI,CAAC,IAAI,KAAK,qBAAqB;QAAE,OAAO,IAAI,CAAC,GAAG,CAAC;IACzD,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,KAAK,mBAAmB;QAChD,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI;QACrB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC;AACrB,CAAC;AAED,MAAM,IAAI,GAAG,WAAW,CAAC,WAAW,CAAC,WAAW,CAAC;IAC/C,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACJ,WAAW,EACT,4FAA4F;SAC/F;QACD,MAAM,EAAE,EAAE;QACV,QAAQ,EAAE;YACR,YAAY,EACV,kOAAkO;SACrO;KACF;IACD,cAAc,EAAE,EAAE;IAClB,MAAM,CAAC,OAAO;QACZ,4EAA4E;QAC5E,MAAM,QAAQ,GAAG,WAAW,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;QACxD,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC;QAClD,MAAM,KAAK,GAAG,CAAC,IAAqC,EAAE,EAAE;YACtD,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;YAC7B,MAAM,IAAI,GAAG,QAAQ,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;YAC9C,MAAM,MAAM,GAAG,QAAQ,CAAC,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACxD,IAAI,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC;gBACxC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC,CAAC;YACtD,CAAC;QACH,CAAC,CAAC;QACF,OAAO;YACL,kBAAkB,EAAE,KAAK;YACzB,gBAAgB,EAAE,KAAK;YACvB,4BAA4B,EAAE,KAAK;YACnC,0BAA0B,EAAE,KAAK;YACjC,mBAAmB,EAAE,KAAK;SAC3B,CAAC;IACJ,CAAC;CACF,CAAC,CAAC;AAEH,eAAe,UAAU,CAAC,IAAI,CAAC,CAAC"}
1
+ {"version":3,"file":"no-promise-field.js","sourceRoot":"","sources":["../../src/async-values/no-promise-field.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zukhruf/eslint",
3
- "version": "0.3.14",
3
+ "version": "0.3.16",
4
4
  "description": "ESLint rules and configs shared across workspaces. Every check is its own named rule, so a repo's own config cannot silently erase it.",
5
5
  "license": "MIT",
6
6
  "repository": {