eslint-rule-forge 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -87,6 +87,27 @@ assertion (`x as string | null`) then makes the rule more cautious, and a
87
87
  narrowing one (`x!`, `v as string` on an `unknown`), which the checker cannot
88
88
  verify, changes nothing.
89
89
 
90
+ ## Negation
91
+
92
+ Writing the negation of a condition the short way, as a fix that removes a
93
+ ternary or turns a filter into a loop has to.
94
+
95
+ | Helper | What it gives |
96
+ | :---------------------------- | :------------------------------------------------------------------------------------------------------------------------ |
97
+ | `negationOf` | `a !== b` for `a === b`, `!a && !b` for `a \|\| b`, `a` for `!a` (when a boolean), else `!a`; parenthesized as an operand |
98
+ | `prefixedNegationOf` | `!a`, `!f(x)`, `!(a === b)`: a `!` in front, with parentheses only when it needs them |
99
+ | `isComparison` | whether a node is an equality or a relational comparison |
100
+ | `isExactlyInvertible` | whether inverting a comparison's operator is its exact negation: always for an equality, for `<` only without `NaN` |
101
+ | `invertedComparisonText` | the comparison's source with its operator inverted |
102
+ | `needsParenthesesAsOperandOf` | whether a node needs parentheses as an operand of `&&` or `\|\|` |
103
+ | `createGetLayerTypes` | a function giving a node's type at every type-wrapper layer, which the checks above ask |
104
+
105
+ `negationOf` gives `undefined` when the only exact negation is `!!a` (for an
106
+ `a` that may not be a boolean) or `!(a < b)` (for operands that may be `NaN`),
107
+ so that a rule can choose between `prefixedNegationOf` and giving up. Where
108
+ only the truthiness of the negation is read, such as the test of an `if`, its
109
+ context may call every node a boolean.
110
+
90
111
  ## Parentheses
91
112
 
92
113
  The range and the text of a node leave out the parentheses written around it.
package/dist/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './bindings.mjs';
2
2
  export * from './imports.mjs';
3
+ export * from './negation.mjs';
3
4
  export * from './parentheses.mjs';
4
5
  export * from './type-wrappers.mjs';
5
6
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC"}
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './bindings.mjs';
2
2
  export * from './imports.mjs';
3
+ export * from './negation.mjs';
3
4
  export * from './parentheses.mjs';
4
5
  export * from './type-wrappers.mjs';
5
6
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.mjs","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC"}
@@ -0,0 +1,102 @@
1
+ import { type ParserServicesWithTypeInformation, type TSESLint, type TSESTree } from '@typescript-eslint/utils';
2
+ import type { DeepReadonly, StrictPick } from 'ts-type-forge';
3
+ import * as ts from 'typescript';
4
+ /** Each comparison operator, and the one that is true exactly when it is not. */
5
+ export declare const INVERTED_COMPARISON_OPERATOR: {
6
+ readonly '===': '!==';
7
+ readonly '!==': '===';
8
+ readonly '==': '!=';
9
+ readonly '!=': '==';
10
+ readonly '<': '>=';
11
+ readonly '>=': '<';
12
+ readonly '>': '<=';
13
+ readonly '<=': '>';
14
+ };
15
+ export type ComparisonOperator = keyof typeof INVERTED_COMPARISON_OPERATOR;
16
+ export type ComparisonExpression = DeepReadonly<TSESTree.BinaryExpression> & Readonly<{
17
+ operator: ComparisonOperator;
18
+ }>;
19
+ /**
20
+ * The type of a node at each of its type-wrapper layers, outermost first (see
21
+ * {@link typeWrapperLayers}).
22
+ */
23
+ export type GetLayerTypes = (node: DeepReadonly<TSESTree.Node>) => readonly ts.Type[];
24
+ /**
25
+ * Builds a {@link GetLayerTypes}: `x as A` gives the types of `x as A` and of
26
+ * `x`.
27
+ *
28
+ * A conclusion a rule draws from a type is drawn only when it holds for every
29
+ * one of them. A widening `as` (`b as boolean | number`) is then the author
30
+ * saying the value may be something else, and is respected, while a narrowing
31
+ * one (`n as unknown as boolean`) leaves the value's own type to decide.
32
+ */
33
+ export declare const createGetLayerTypes: (services: StrictPick<ParserServicesWithTypeInformation, 'esTreeNodeToTSNodeMap' | 'program'>) => GetLayerTypes;
34
+ export declare const isComparison: (node: DeepReadonly<TSESTree.Node>) => node is ComparisonExpression;
35
+ /**
36
+ * Whether inverting the operator gives exactly the negation.
37
+ *
38
+ * An equality always does. A relational comparison does unless an operand
39
+ * converts to `NaN`: every relational comparison with `NaN` is `false`, so
40
+ * `!(a < b)` is `true` there while `a >= b` is `false`. It cannot when both
41
+ * sides are strings (compared as strings), or both are numeric values that are
42
+ * never `NaN` — number literals, `bigint`, booleans, and number brands that
43
+ * declare `NaNValue: false` (`ValidNumber`, `Int`, `FiniteNumber`, …).
44
+ *
45
+ * `getLayerTypes` gives an operand's type at each of its type-wrapper layers,
46
+ * and every one of them has to say so: `(v as number) < 1` may be `NaN` even
47
+ * when `v` is a `ValidNumber`, and `(n as ValidNumber) < 1` still may when `n`
48
+ * is a `number`.
49
+ */
50
+ export declare const isExactlyInvertible: (node: ComparisonExpression, getLayerTypes: GetLayerTypes, checker: ts.TypeChecker) => boolean;
51
+ /**
52
+ * The source of `node` with its operator inverted, and everything else —
53
+ * operands, comments, line breaks — as written.
54
+ */
55
+ export declare const invertedComparisonText: (node: ComparisonExpression, sourceCode: DeepReadonly<TSESLint.SourceCode>) => string;
56
+ /** What {@link negationOf} needs to know about the code it negates. */
57
+ export type NegationContext = Readonly<{
58
+ sourceCode: DeepReadonly<TSESLint.SourceCode>;
59
+ checker: ts.TypeChecker;
60
+ getLayerTypes: GetLayerTypes;
61
+ /**
62
+ * Whether `node`'s value is a boolean, so that the negation of `!node` may
63
+ * be `node` itself. Where only the truthiness of the negation is read — the
64
+ * test of an `if` — any value will do, and this may answer `true` for every
65
+ * node.
66
+ */
67
+ isBoolean: (node: DeepReadonly<TSESTree.Expression>) => boolean;
68
+ }>;
69
+ /**
70
+ * The negation of `node`, written the short way, as the source text of an
71
+ * operand of `operandOf` (parenthesized if need be), or of a whole expression
72
+ * when `operandOf` is `undefined`:
73
+ *
74
+ * | `node` | negation | when |
75
+ * | :----------------- | :---------------- | :------------------------------------ |
76
+ * | `!a` | `a` | `a` is a boolean (`context.isBoolean`) |
77
+ * | `a === b` | `a !== b` | always |
78
+ * | `a < b` | `a >= b` | no operand can be `NaN` |
79
+ * | `a && b`, `a \|\| b` | `!a \|\| !b`, `!a && !b` | always, each side negated the same way |
80
+ * | anything else | `!a`, `!(a)` | always |
81
+ *
82
+ * `undefined` when the first two rows' condition does not hold: then the only
83
+ * exact negation is `!!a` or `!(a < b)`, and a caller decides whether to
84
+ * write it (with {@link prefixedNegationOf}) or to give up. Inside `&&` and
85
+ * `||`, a side with no short negation is written with `!`.
86
+ *
87
+ * The node is taken as written: a comparison under a type wrapper is not
88
+ * looked into, since inverting it would drop the wrapper.
89
+ */
90
+ export declare const negationOf: (node: DeepReadonly<TSESTree.Expression>, context: NegationContext, operandOf: '&&' | '||' | undefined) => string | undefined;
91
+ /**
92
+ * `!node`, with `node` parenthesized unless `!` already binds to all of it:
93
+ * `!a`, `!f(x)`, `!(a === b)`.
94
+ */
95
+ export declare const prefixedNegationOf: (node: DeepReadonly<TSESTree.Expression>, sourceCode: DeepReadonly<TSESLint.SourceCode>) => string;
96
+ /**
97
+ * Whether `node`, written as an operand of `operator`, needs parentheses to
98
+ * stay one operand: `a || b` under `&&`, `a ?? b` under either, and anything
99
+ * binding looser than both (`a ? b : c`, `a = b`, …).
100
+ */
101
+ export declare const needsParenthesesAsOperandOf: (node: DeepReadonly<TSESTree.Node>, operator: '&&' | '||') => boolean;
102
+ //# sourceMappingURL=negation.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"negation.d.mts","sourceRoot":"","sources":["../src/negation.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,iCAAiC,EACtC,KAAK,QAAQ,EACb,KAAK,QAAQ,EACd,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AAKjC,iFAAiF;AACjF,eAAO,MAAM,4BAA4B;aACvC,KAAK,EAAE,KAAK;aACZ,KAAK,EAAE,KAAK;aACZ,IAAI,EAAE,IAAI;aACV,IAAI,EAAE,IAAI;aACV,GAAG,EAAE,IAAI;aACT,IAAI,EAAE,GAAG;aACT,GAAG,EAAE,IAAI;aACT,IAAI,EAAE,GAAG;CACD,CAAC;AAEX,MAAM,MAAM,kBAAkB,GAAG,MAAM,OAAO,4BAA4B,CAAC;AAE3E,MAAM,MAAM,oBAAoB,GAAG,YAAY,CAAC,QAAQ,CAAC,gBAAgB,CAAC,GACxE,QAAQ,CAAC;IAAE,QAAQ,EAAE,kBAAkB,CAAA;CAAE,CAAC,CAAC;AAE7C;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,IAAI,EAAE,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,KAC9B,SAAS,EAAE,CAAC,IAAI,EAAE,CAAC;AAExB;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,aAElB,UAAU,CAClB,iCAAiC,EACjC,uBAAuB,GAAG,SAAS,CACpC,KACA,aASF,CAAC;AAEJ,eAAO,MAAM,YAAY,SACjB,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,KAChC,IAAI,IAAI,oBAEiD,CAAC;AAE7D;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,mBAAmB,SACxB,oBAAoB,iBACX,aAAa,WACnB,EAAE,CAAC,WAAW,KACtB,OAcF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,sBAAsB,SAC3B,oBAAoB,cACd,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC,KAC5C,MAoBF,CAAC;AAEF,uEAAuE;AACvE,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IACrC,UAAU,EAAE,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC9C,OAAO,EAAE,EAAE,CAAC,WAAW,CAAC;IACxB,aAAa,EAAE,aAAa,CAAC;IAC7B;;;;;OAKG;IACH,SAAS,EAAE,CAAC,IAAI,EAAE,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC,KAAK,OAAO,CAAC;CACjE,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,UAAU,SACf,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC,WAC9B,eAAe,aACb,IAAI,GAAG,IAAI,GAAG,SAAS,KACjC,MAAM,GAAG,SAIX,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,SACvB,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC,cAC3B,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC,KAC5C,MAIF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,SAChC,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,YACvB,IAAI,GAAG,IAAI,KACpB,OAAqD,CAAC"}
@@ -0,0 +1,215 @@
1
+ import { AST_NODE_TYPES, } from '@typescript-eslint/utils';
2
+ import * as ts from 'typescript';
3
+ import { typeWrapperLayers } from './type-wrappers.mjs';
4
+
5
+
6
+ export const INVERTED_COMPARISON_OPERATOR = {
7
+ '===': '!==',
8
+ '!==': '===',
9
+ '==': '!=',
10
+ '!=': '==',
11
+ '<': '>=',
12
+ '>=': '<',
13
+ '>': '<=',
14
+ '<=': '>',
15
+ };
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+ export const createGetLayerTypes = (services) => (node) => {
26
+ const checker = services.program.getTypeChecker();
27
+ return typeWrapperLayers(node).map((layer) => checker.getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(asNode(layer))));
28
+ };
29
+ export const isComparison = (node) => node.type === AST_NODE_TYPES.BinaryExpression &&
30
+ Object.hasOwn(INVERTED_COMPARISON_OPERATOR, node.operator);
31
+
32
+
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ export const isExactlyInvertible = (node, getLayerTypes, checker) => {
47
+ if (!RELATIONAL_OPERATORS.has(node.operator)) {
48
+ return true;
49
+ }
50
+ const sides = [
51
+ ...getLayerTypes(node.left),
52
+ ...getLayerTypes(node.right),
53
+ ].flatMap(constituentsOf);
54
+ return (sides.every((type) => (type.flags & ts.TypeFlags.StringLike) !== 0) ||
55
+ sides.every((type) => isNeverNaN(type, checker)));
56
+ };
57
+
58
+
59
+
60
+
61
+ export const invertedComparisonText = (node, sourceCode) => {
62
+ const text = sourceCode.getText(asNode(node));
63
+ const operator = sourceCode.getFirstTokenBetween(asNode(node.left), asNode(node.right), (token) => token.value === node.operator);
64
+ if (operator === null) {
65
+ return `${sourceCode.getText(asNode(node.left))} ${INVERTED_COMPARISON_OPERATOR[node.operator]} ${sourceCode.getText(asNode(node.right))}`;
66
+ }
67
+ const [start] = node.range;
68
+ return [
69
+ text.slice(0, operator.range[0] - start),
70
+ INVERTED_COMPARISON_OPERATOR[node.operator],
71
+ text.slice(operator.range[1] - start),
72
+ ].join('');
73
+ };
74
+
75
+
76
+
77
+
78
+
79
+
80
+
81
+
82
+
83
+
84
+
85
+
86
+
87
+
88
+
89
+
90
+
91
+
92
+
93
+
94
+
95
+ export const negationOf = (node, context, operandOf) => {
96
+ const negation = shortNegationOf(node, context);
97
+ return negation === undefined ? undefined : asOperand(negation, operandOf);
98
+ };
99
+
100
+
101
+
102
+
103
+ export const prefixedNegationOf = (node, sourceCode) => {
104
+ const text = sourceCode.getText(asNode(node));
105
+ return BINDS_AS_TIGHT_AS_UNARY.has(node.type) ? `!${text}` : `!(${text})`;
106
+ };
107
+
108
+
109
+
110
+
111
+
112
+ export const needsParenthesesAsOperandOf = (node, operator) => isLooserThan(precedenceOf(node), operator);
113
+ const shortNegationOf = (node, context) => {
114
+ if (node.type === AST_NODE_TYPES.UnaryExpression && node.operator === '!') {
115
+ return context.isBoolean(node.argument)
116
+ ? {
117
+ text: context.sourceCode.getText(asNode(node.argument)),
118
+ precedence: precedenceOf(node.argument),
119
+ }
120
+ : undefined;
121
+ }
122
+ if (isComparison(node)) {
123
+ return isExactlyInvertible(node, context.getLayerTypes, context.checker)
124
+ ? {
125
+ text: invertedComparisonText(node, context.sourceCode),
126
+ precedence: 'tighter',
127
+ }
128
+ : undefined;
129
+ }
130
+ if (node.type === AST_NODE_TYPES.LogicalExpression &&
131
+ node.operator !== '??') {
132
+ const operator = node.operator === '&&' ? '||' : '&&';
133
+ const side = (operand) => asOperand(shortNegationOf(operand, context) ?? {
134
+ text: prefixedNegationOf(operand, context.sourceCode),
135
+ precedence: 'tighter',
136
+ }, operator);
137
+ return {
138
+ text: `${side(node.left)} ${operator} ${side(node.right)}`,
139
+ precedence: operator,
140
+ };
141
+ }
142
+ return {
143
+ text: prefixedNegationOf(node, context.sourceCode),
144
+ precedence: 'tighter',
145
+ };
146
+ };
147
+ const asOperand = (negation, operator) => operator !== undefined && isLooserThan(negation.precedence, operator)
148
+ ? `(${negation.text})`
149
+ : negation.text;
150
+ const isLooserThan = (precedence, operator) => precedence === 'looser' || (operator === '&&' && precedence === '||');
151
+ const precedenceOf = (node) => {
152
+ if (node.type === AST_NODE_TYPES.LogicalExpression) {
153
+
154
+ return node.operator === '??' ? 'looser' : node.operator;
155
+ }
156
+ return LOOSER_THAN_LOGICAL.has(node.type) ? 'looser' : 'tighter';
157
+ };
158
+ const LOOSER_THAN_LOGICAL = new Set([
159
+ AST_NODE_TYPES.ArrowFunctionExpression,
160
+ AST_NODE_TYPES.AssignmentExpression,
161
+ AST_NODE_TYPES.ConditionalExpression,
162
+ AST_NODE_TYPES.SequenceExpression,
163
+ AST_NODE_TYPES.YieldExpression,
164
+ ]);
165
+
166
+ const BINDS_AS_TIGHT_AS_UNARY = new Set([
167
+ AST_NODE_TYPES.ArrayExpression,
168
+ AST_NODE_TYPES.AwaitExpression,
169
+ AST_NODE_TYPES.CallExpression,
170
+ AST_NODE_TYPES.ChainExpression,
171
+ AST_NODE_TYPES.Identifier,
172
+ AST_NODE_TYPES.Literal,
173
+ AST_NODE_TYPES.MemberExpression,
174
+ AST_NODE_TYPES.MetaProperty,
175
+ AST_NODE_TYPES.NewExpression,
176
+ AST_NODE_TYPES.ObjectExpression,
177
+ AST_NODE_TYPES.TaggedTemplateExpression,
178
+ AST_NODE_TYPES.TemplateLiteral,
179
+ AST_NODE_TYPES.ThisExpression,
180
+ AST_NODE_TYPES.TSNonNullExpression,
181
+ AST_NODE_TYPES.UnaryExpression,
182
+ ]);
183
+ const RELATIONAL_OPERATORS = new Set(['<', '<=', '>', '>=']);
184
+ const constituentsOf = (type) => type.isUnion() ? type.types : [type];
185
+ const isNeverNaN = (type, checker) => {
186
+
187
+
188
+
189
+ const neverNaNFlags = ts.TypeFlags.NumberLiteral |
190
+ ts.TypeFlags.BigIntLike |
191
+ ts.TypeFlags.BooleanLike |
192
+ ts.TypeFlags.Null;
193
+ if ((type.flags & neverNaNFlags) !== 0) {
194
+ return true;
195
+ }
196
+ if (!type.isIntersection()) {
197
+ return false;
198
+ }
199
+ const brand = checker.getPropertyOfType(type, 'NaNValue');
200
+ if (brand === undefined) {
201
+ return false;
202
+ }
203
+ const brandType = checker.getTypeOfSymbol(brand);
204
+ return (type.types.some((member) => (member.flags & ts.TypeFlags.NumberLike) !== 0) &&
205
+ (brandType.flags & ts.TypeFlags.BooleanLiteral) !== 0 &&
206
+ checker.typeToString(brandType) === 'false');
207
+ };
208
+
209
+
210
+
211
+
212
+ const asNode = (node) =>
213
+
214
+ node;
215
+ //# sourceMappingURL=negation.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"negation.mjs","sourceRoot":"","sources":["../src/negation.mts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-rule-forge",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "description": "Helpers for writing typescript-eslint rules that see through type wrappers and parentheses, and resolve what a file imports.",
6
6
  "keywords": [
@@ -41,13 +41,13 @@
41
41
  "@typescript-eslint/rule-tester": "8.71.1",
42
42
  "dedent": "1.7.2",
43
43
  "eslint": "10.12.0",
44
- "eslint-config-typed": "5.17.4",
45
- "eslint-plugin-ts-fortress": "1.3.3",
46
- "eslint-plugin-ts-type-forge": "0.8.8",
44
+ "eslint-config-typed": "5.19.0",
45
+ "eslint-plugin-ts-fortress": "1.3.4",
46
+ "eslint-plugin-ts-type-forge": "0.8.9",
47
47
  "prettier": "3.9.9",
48
48
  "prettier-plugin-organize-imports": "4.3.0",
49
49
  "prettier-plugin-packagejson": "3.0.2",
50
- "ts-data-forge": "14.7.1",
50
+ "ts-data-forge": "14.8.0",
51
51
  "ts-repo-utils": "10.7.1"
52
52
  },
53
53
  "peerDependencies": {
@@ -65,15 +65,15 @@
65
65
  "scripts": {
66
66
  "build": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/build.mts",
67
67
  "check:ext": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/check-ext.mts",
68
- "check:lint": "oxlint . && eslint .",
69
- "check:test": "pnpm run z:vitest run",
70
- "check:test:cov": "pnpm run z:vitest run --coverage",
71
- "check:types": "node ../../node_modules/typescript-native/bin/tsc --noEmit",
68
+ "check:lint": "node ../../tools/scripts/cmd/assert-deps-built.mts && oxlint . && eslint .",
69
+ "check:test": "node ../../tools/scripts/cmd/assert-deps-built.mts && pnpm run z:vitest run",
70
+ "check:test:cov": "node ../../tools/scripts/cmd/assert-deps-built.mts && pnpm run z:vitest run --coverage",
71
+ "check:types": "node ../../tools/scripts/cmd/assert-deps-built.mts && node ../../node_modules/typescript-native/bin/tsc --noEmit",
72
72
  "doc": "pnpm run gen:readme",
73
73
  "fix:fmt": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-repo-utils/src/cmd/format-uncommitted.mts --cwd .",
74
74
  "fix:fmt:diff": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-repo-utils/src/cmd/format-diff-from.mts origin/main --cwd .",
75
75
  "fix:fmt:full": "pnpm run z:prettier .",
76
- "fix:lint": "oxlint . --fix && eslint . --fix",
76
+ "fix:lint": "node ../../tools/scripts/cmd/assert-deps-built.mts && oxlint . --fix && eslint . --fix",
77
77
  "fmt": "pnpm run fix:fmt",
78
78
  "gen": "run-s gen:index gen:readme",
79
79
  "gen:index": "run-s gen:index:src fix:fmt",
package/src/index.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './bindings.mjs';
2
2
  export * from './imports.mjs';
3
+ export * from './negation.mjs';
3
4
  export * from './parentheses.mjs';
4
5
  export * from './type-wrappers.mjs';
@@ -0,0 +1,356 @@
1
+ import {
2
+ AST_NODE_TYPES,
3
+ type ParserServicesWithTypeInformation,
4
+ type TSESLint,
5
+ type TSESTree,
6
+ } from '@typescript-eslint/utils';
7
+ import type { DeepReadonly, StrictPick } from 'ts-type-forge';
8
+ import * as ts from 'typescript';
9
+ import { typeWrapperLayers } from './type-wrappers.mjs';
10
+
11
+ /* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */
12
+
13
+ /** Each comparison operator, and the one that is true exactly when it is not. */
14
+ export const INVERTED_COMPARISON_OPERATOR = {
15
+ '===': '!==',
16
+ '!==': '===',
17
+ '==': '!=',
18
+ '!=': '==',
19
+ '<': '>=',
20
+ '>=': '<',
21
+ '>': '<=',
22
+ '<=': '>',
23
+ } as const;
24
+
25
+ export type ComparisonOperator = keyof typeof INVERTED_COMPARISON_OPERATOR;
26
+
27
+ export type ComparisonExpression = DeepReadonly<TSESTree.BinaryExpression> &
28
+ Readonly<{ operator: ComparisonOperator }>;
29
+
30
+ /**
31
+ * The type of a node at each of its type-wrapper layers, outermost first (see
32
+ * {@link typeWrapperLayers}).
33
+ */
34
+ export type GetLayerTypes = (
35
+ node: DeepReadonly<TSESTree.Node>,
36
+ ) => readonly ts.Type[];
37
+
38
+ /**
39
+ * Builds a {@link GetLayerTypes}: `x as A` gives the types of `x as A` and of
40
+ * `x`.
41
+ *
42
+ * A conclusion a rule draws from a type is drawn only when it holds for every
43
+ * one of them. A widening `as` (`b as boolean | number`) is then the author
44
+ * saying the value may be something else, and is respected, while a narrowing
45
+ * one (`n as unknown as boolean`) leaves the value's own type to decide.
46
+ */
47
+ export const createGetLayerTypes =
48
+ (
49
+ services: StrictPick<
50
+ ParserServicesWithTypeInformation,
51
+ 'esTreeNodeToTSNodeMap' | 'program'
52
+ >,
53
+ ): GetLayerTypes =>
54
+ (node) => {
55
+ const checker = services.program.getTypeChecker();
56
+
57
+ return typeWrapperLayers(node).map((layer) =>
58
+ checker.getTypeAtLocation(
59
+ services.esTreeNodeToTSNodeMap.get(asNode(layer)),
60
+ ),
61
+ );
62
+ };
63
+
64
+ export const isComparison = (
65
+ node: DeepReadonly<TSESTree.Node>,
66
+ ): node is ComparisonExpression =>
67
+ node.type === AST_NODE_TYPES.BinaryExpression &&
68
+ Object.hasOwn(INVERTED_COMPARISON_OPERATOR, node.operator);
69
+
70
+ /**
71
+ * Whether inverting the operator gives exactly the negation.
72
+ *
73
+ * An equality always does. A relational comparison does unless an operand
74
+ * converts to `NaN`: every relational comparison with `NaN` is `false`, so
75
+ * `!(a < b)` is `true` there while `a >= b` is `false`. It cannot when both
76
+ * sides are strings (compared as strings), or both are numeric values that are
77
+ * never `NaN` — number literals, `bigint`, booleans, and number brands that
78
+ * declare `NaNValue: false` (`ValidNumber`, `Int`, `FiniteNumber`, …).
79
+ *
80
+ * `getLayerTypes` gives an operand's type at each of its type-wrapper layers,
81
+ * and every one of them has to say so: `(v as number) < 1` may be `NaN` even
82
+ * when `v` is a `ValidNumber`, and `(n as ValidNumber) < 1` still may when `n`
83
+ * is a `number`.
84
+ */
85
+ export const isExactlyInvertible = (
86
+ node: ComparisonExpression,
87
+ getLayerTypes: GetLayerTypes,
88
+ checker: ts.TypeChecker,
89
+ ): boolean => {
90
+ if (!RELATIONAL_OPERATORS.has(node.operator)) {
91
+ return true;
92
+ }
93
+
94
+ const sides = [
95
+ ...getLayerTypes(node.left),
96
+ ...getLayerTypes(node.right),
97
+ ].flatMap(constituentsOf);
98
+
99
+ return (
100
+ sides.every((type) => (type.flags & ts.TypeFlags.StringLike) !== 0) ||
101
+ sides.every((type) => isNeverNaN(type, checker))
102
+ );
103
+ };
104
+
105
+ /**
106
+ * The source of `node` with its operator inverted, and everything else —
107
+ * operands, comments, line breaks — as written.
108
+ */
109
+ export const invertedComparisonText = (
110
+ node: ComparisonExpression,
111
+ sourceCode: DeepReadonly<TSESLint.SourceCode>,
112
+ ): string => {
113
+ const text = sourceCode.getText(asNode(node));
114
+
115
+ const operator = sourceCode.getFirstTokenBetween(
116
+ asNode(node.left),
117
+ asNode(node.right),
118
+ (token) => token.value === node.operator,
119
+ );
120
+
121
+ if (operator === null) {
122
+ return `${sourceCode.getText(asNode(node.left))} ${INVERTED_COMPARISON_OPERATOR[node.operator]} ${sourceCode.getText(asNode(node.right))}`;
123
+ }
124
+
125
+ const [start] = node.range;
126
+
127
+ return [
128
+ text.slice(0, operator.range[0] - start),
129
+ INVERTED_COMPARISON_OPERATOR[node.operator],
130
+ text.slice(operator.range[1] - start),
131
+ ].join('');
132
+ };
133
+
134
+ /** What {@link negationOf} needs to know about the code it negates. */
135
+ export type NegationContext = Readonly<{
136
+ sourceCode: DeepReadonly<TSESLint.SourceCode>;
137
+ checker: ts.TypeChecker;
138
+ getLayerTypes: GetLayerTypes;
139
+ /**
140
+ * Whether `node`'s value is a boolean, so that the negation of `!node` may
141
+ * be `node` itself. Where only the truthiness of the negation is read — the
142
+ * test of an `if` — any value will do, and this may answer `true` for every
143
+ * node.
144
+ */
145
+ isBoolean: (node: DeepReadonly<TSESTree.Expression>) => boolean;
146
+ }>;
147
+
148
+ /**
149
+ * The negation of `node`, written the short way, as the source text of an
150
+ * operand of `operandOf` (parenthesized if need be), or of a whole expression
151
+ * when `operandOf` is `undefined`:
152
+ *
153
+ * | `node` | negation | when |
154
+ * | :----------------- | :---------------- | :------------------------------------ |
155
+ * | `!a` | `a` | `a` is a boolean (`context.isBoolean`) |
156
+ * | `a === b` | `a !== b` | always |
157
+ * | `a < b` | `a >= b` | no operand can be `NaN` |
158
+ * | `a && b`, `a \|\| b` | `!a \|\| !b`, `!a && !b` | always, each side negated the same way |
159
+ * | anything else | `!a`, `!(a)` | always |
160
+ *
161
+ * `undefined` when the first two rows' condition does not hold: then the only
162
+ * exact negation is `!!a` or `!(a < b)`, and a caller decides whether to
163
+ * write it (with {@link prefixedNegationOf}) or to give up. Inside `&&` and
164
+ * `||`, a side with no short negation is written with `!`.
165
+ *
166
+ * The node is taken as written: a comparison under a type wrapper is not
167
+ * looked into, since inverting it would drop the wrapper.
168
+ */
169
+ export const negationOf = (
170
+ node: DeepReadonly<TSESTree.Expression>,
171
+ context: NegationContext,
172
+ operandOf: '&&' | '||' | undefined,
173
+ ): string | undefined => {
174
+ const negation = shortNegationOf(node, context);
175
+
176
+ return negation === undefined ? undefined : asOperand(negation, operandOf);
177
+ };
178
+
179
+ /**
180
+ * `!node`, with `node` parenthesized unless `!` already binds to all of it:
181
+ * `!a`, `!f(x)`, `!(a === b)`.
182
+ */
183
+ export const prefixedNegationOf = (
184
+ node: DeepReadonly<TSESTree.Expression>,
185
+ sourceCode: DeepReadonly<TSESLint.SourceCode>,
186
+ ): string => {
187
+ const text = sourceCode.getText(asNode(node));
188
+
189
+ return BINDS_AS_TIGHT_AS_UNARY.has(node.type) ? `!${text}` : `!(${text})`;
190
+ };
191
+
192
+ /**
193
+ * Whether `node`, written as an operand of `operator`, needs parentheses to
194
+ * stay one operand: `a || b` under `&&`, `a ?? b` under either, and anything
195
+ * binding looser than both (`a ? b : c`, `a = b`, …).
196
+ */
197
+ export const needsParenthesesAsOperandOf = (
198
+ node: DeepReadonly<TSESTree.Node>,
199
+ operator: '&&' | '||',
200
+ ): boolean => isLooserThan(precedenceOf(node), operator);
201
+
202
+ /**
203
+ * How tightly an expression binds, as far as putting it under `&&` or `||` is
204
+ * concerned.
205
+ */
206
+ type Precedence = '&&' | '||' | 'looser' | 'tighter';
207
+
208
+ type Negation = Readonly<{
209
+ text: string;
210
+ precedence: Precedence;
211
+ }>;
212
+
213
+ const shortNegationOf = (
214
+ node: DeepReadonly<TSESTree.Expression>,
215
+ context: NegationContext,
216
+ ): Negation | undefined => {
217
+ if (node.type === AST_NODE_TYPES.UnaryExpression && node.operator === '!') {
218
+ return context.isBoolean(node.argument)
219
+ ? {
220
+ text: context.sourceCode.getText(asNode(node.argument)),
221
+ precedence: precedenceOf(node.argument),
222
+ }
223
+ : undefined;
224
+ }
225
+
226
+ if (isComparison(node)) {
227
+ return isExactlyInvertible(node, context.getLayerTypes, context.checker)
228
+ ? {
229
+ text: invertedComparisonText(node, context.sourceCode),
230
+ precedence: 'tighter',
231
+ }
232
+ : undefined;
233
+ }
234
+
235
+ if (
236
+ node.type === AST_NODE_TYPES.LogicalExpression &&
237
+ node.operator !== '??'
238
+ ) {
239
+ const operator = node.operator === '&&' ? '||' : '&&';
240
+
241
+ const side = (operand: DeepReadonly<TSESTree.Expression>): string =>
242
+ asOperand(
243
+ shortNegationOf(operand, context) ?? {
244
+ text: prefixedNegationOf(operand, context.sourceCode),
245
+ precedence: 'tighter',
246
+ },
247
+ operator,
248
+ );
249
+
250
+ return {
251
+ text: `${side(node.left)} ${operator} ${side(node.right)}`,
252
+ precedence: operator,
253
+ };
254
+ }
255
+
256
+ return {
257
+ text: prefixedNegationOf(node, context.sourceCode),
258
+ precedence: 'tighter',
259
+ };
260
+ };
261
+
262
+ const asOperand = (
263
+ negation: Negation,
264
+ operator: '&&' | '||' | undefined,
265
+ ): string =>
266
+ operator !== undefined && isLooserThan(negation.precedence, operator)
267
+ ? (`(${negation.text})` as const)
268
+ : negation.text;
269
+
270
+ const isLooserThan = (precedence: Precedence, operator: '&&' | '||'): boolean =>
271
+ precedence === 'looser' || (operator === '&&' && precedence === '||');
272
+
273
+ const precedenceOf = (node: DeepReadonly<TSESTree.Node>): Precedence => {
274
+ if (node.type === AST_NODE_TYPES.LogicalExpression) {
275
+ // `??` cannot be mixed with `&&` or `||` without parentheses.
276
+ return node.operator === '??' ? 'looser' : node.operator;
277
+ }
278
+
279
+ return LOOSER_THAN_LOGICAL.has(node.type) ? 'looser' : 'tighter';
280
+ };
281
+
282
+ const LOOSER_THAN_LOGICAL: ReadonlySet<AST_NODE_TYPES> = new Set([
283
+ AST_NODE_TYPES.ArrowFunctionExpression,
284
+ AST_NODE_TYPES.AssignmentExpression,
285
+ AST_NODE_TYPES.ConditionalExpression,
286
+ AST_NODE_TYPES.SequenceExpression,
287
+ AST_NODE_TYPES.YieldExpression,
288
+ ]);
289
+
290
+ /** Expressions `!` can be put in front of without parentheses. */
291
+ const BINDS_AS_TIGHT_AS_UNARY: ReadonlySet<AST_NODE_TYPES> = new Set([
292
+ AST_NODE_TYPES.ArrayExpression,
293
+ AST_NODE_TYPES.AwaitExpression,
294
+ AST_NODE_TYPES.CallExpression,
295
+ AST_NODE_TYPES.ChainExpression,
296
+ AST_NODE_TYPES.Identifier,
297
+ AST_NODE_TYPES.Literal,
298
+ AST_NODE_TYPES.MemberExpression,
299
+ AST_NODE_TYPES.MetaProperty,
300
+ AST_NODE_TYPES.NewExpression,
301
+ AST_NODE_TYPES.ObjectExpression,
302
+ AST_NODE_TYPES.TaggedTemplateExpression,
303
+ AST_NODE_TYPES.TemplateLiteral,
304
+ AST_NODE_TYPES.ThisExpression,
305
+ AST_NODE_TYPES.TSNonNullExpression,
306
+ AST_NODE_TYPES.UnaryExpression,
307
+ ]);
308
+
309
+ const RELATIONAL_OPERATORS: ReadonlySet<ComparisonOperator> =
310
+ new Set<ComparisonOperator>(['<', '<=', '>', '>=']);
311
+
312
+ const constituentsOf = (type: ts.Type): readonly ts.Type[] =>
313
+ type.isUnion() ? type.types : ([type] as const);
314
+
315
+ const isNeverNaN = (type: ts.Type, checker: ts.TypeChecker): boolean => {
316
+ // Read `ts.TypeFlags` here, not at module scope: every importer of this
317
+ // package's index evaluates the module, and a `typescript` without the JS
318
+ // API (7.x) would fail the import even for rules that never get here.
319
+ const neverNaNFlags =
320
+ ts.TypeFlags.NumberLiteral |
321
+ ts.TypeFlags.BigIntLike |
322
+ ts.TypeFlags.BooleanLike |
323
+ ts.TypeFlags.Null;
324
+
325
+ if ((type.flags & neverNaNFlags) !== 0) {
326
+ return true;
327
+ }
328
+
329
+ if (!type.isIntersection()) {
330
+ return false;
331
+ }
332
+
333
+ const brand = checker.getPropertyOfType(type, 'NaNValue');
334
+
335
+ if (brand === undefined) {
336
+ return false;
337
+ }
338
+
339
+ const brandType = checker.getTypeOfSymbol(brand);
340
+
341
+ return (
342
+ type.types.some(
343
+ (member) => (member.flags & ts.TypeFlags.NumberLike) !== 0,
344
+ ) &&
345
+ (brandType.flags & ts.TypeFlags.BooleanLiteral) !== 0 &&
346
+ checker.typeToString(brandType) === 'false'
347
+ );
348
+ };
349
+
350
+ /**
351
+ * The parser's node, which the source code and the token store take as
352
+ * mutable, for one that this package only reads.
353
+ */
354
+ const asNode = <T extends TSESTree.Node>(node: DeepReadonly<T>): T =>
355
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
356
+ node as T;