@zukhruf/eslint 0.3.13 → 0.3.15
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":";
|
|
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>[]`)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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(
|
|
11
|
-
if (
|
|
69
|
+
function holdsPromise(walk, type, depth = 0, path = new Set()) {
|
|
70
|
+
if (path.has(type))
|
|
12
71
|
return false;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
.
|
|
19
|
-
.some((
|
|
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
|
|
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
|
|
49
|
-
|
|
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;
|
|
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.
|
|
3
|
+
"version": "0.3.15",
|
|
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": {
|