Skip to content

Commit 78c5abf

Browse files
authored
consistent-boolean-name: Add wrappers option (#3572)
1 parent a09869b commit 78c5abf

6 files changed

Lines changed: 441 additions & 94 deletions

docs/rules/consistent-boolean-name.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Boolean names should start with a prefix that makes the boolean meaning clear.
1515

1616
Names that start with a boolean prefix should also refer to booleans or boolean-returning functions. Unknown values are ignored.
1717

18+
Configured wrapper bindings may use boolean prefixes when a configured property or method provides a boolean-like value. This applies only to variables and parameters that are not reassigned.
19+
1820
Reports for property and method names, and reports for non-boolean values using boolean prefixes, do not provide rename suggestions.
1921

2022
The default prefixes are:
@@ -231,6 +233,44 @@ And this would fail:
231233
const didUpdate = true;
232234
```
233235

236+
### wrappers
237+
238+
Type: `Record<string, string>`\
239+
Default: `{}`
240+
241+
Map unqualified TypeScript wrapper type names to the property or method that provides a boolean-like value. Same-named types share configuration. Derived types, intersections, and constrained type parameters are supported. Members may provide `boolean`, `Promise<boolean>`, or `PromiseLike<boolean>`; nullable results are accepted. Requires TypeScript type information.
242+
243+
```js
244+
'unicorn/consistent-boolean-name': [
245+
'error',
246+
{
247+
wrappers: {
248+
StorageItem: 'get',
249+
},
250+
},
251+
]
252+
```
253+
254+
With the above configuration, this would pass:
255+
256+
```ts
257+
interface StorageItem<Base, Return = Base | undefined> {
258+
get(): Promise<Return>;
259+
}
260+
261+
declare const isUnicorn: StorageItem<unknown, boolean>;
262+
```
263+
264+
But this would still fail because `get()` returns a string:
265+
266+
```ts
267+
interface StorageItem<Base, Return = Base | undefined> {
268+
get(): Promise<Return>;
269+
}
270+
271+
declare const isUnicorn: StorageItem<unknown, string>;
272+
```
273+
234274
### ignore
235275

236276
Type: `Array<string | RegExp>`\

rules/consistent-boolean-name.js

Lines changed: 26 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import {isRegExp} from 'node:util/types';
22
import {findVariable, getPropertyName, getStaticValue} from '@eslint-community/eslint-utils';
33
import {renameVariable} from './fix/index.js';
4+
import {combineBooleanStates, getPromisedTypeBooleanState, getTypeBooleanState} from './utils/get-type-boolean-state.js';
45
import resolveVariableName from './utils/resolve-variable-name.js';
6+
import {getBooleanWrapperVariableState} from './utils/get-boolean-wrapper-variable-state.js';
57
import {
68
getAvailableVariableName,
79
getScopes,
810
getVariableIdentifiers,
911
isReactHookName,
10-
isNullishType,
11-
isTypeParameterType,
12-
isUnknownType,
1312
lowerFirst,
1413
upperFirst,
1514
} from './utils/index.js';
@@ -145,10 +144,12 @@ const prepareOptions = ({
145144
checkProperties = false,
146145
prefixes = {},
147146
ignore = [],
147+
wrappers = {},
148148
} = {}) => ({
149149
checkProperties,
150150
prefixes: getEnabledPrefixes({prefixes}),
151151
ignore: ignore.map(pattern => isRegExp(pattern) ? pattern : new RegExp(pattern, 'u')),
152+
wrappers: new Map(Object.entries(wrappers)),
152153
});
153154

154155
function isIgnoredName(name, ignore) {
@@ -430,17 +431,6 @@ function isBooleanVueReferenceVariable(variable, context) {
430431
&& getExpressionBooleanState(argument, context) === boolean;
431432
}
432433

433-
function combineBooleanStates(states) {
434-
if (
435-
states.length === 0
436-
|| states.includes(unknown)
437-
) {
438-
return unknown;
439-
}
440-
441-
return states.every(state => state === boolean) ? boolean : nonBoolean;
442-
}
443-
444434
function combineVariableBooleanStates(states) {
445435
if (states.includes(nonBoolean)) {
446436
return nonBoolean;
@@ -449,75 +439,6 @@ function combineVariableBooleanStates(states) {
449439
return combineBooleanStates(states);
450440
}
451441

452-
function getTypeBooleanState(type, checker, visitedTypes = new Set(), functionTypesAreBoolean = true) {
453-
if (!type) {
454-
return unknown;
455-
}
456-
457-
if (
458-
isUnknownType(type)
459-
|| type.intrinsicName === 'never'
460-
) {
461-
return unknown;
462-
}
463-
464-
if (visitedTypes.has(type)) {
465-
return unknown;
466-
}
467-
468-
visitedTypes.add(type);
469-
470-
if (isTypeParameterType(type)) {
471-
const constraint = type.getConstraint();
472-
const result = constraint ? getTypeBooleanState(constraint, checker, visitedTypes, functionTypesAreBoolean) : unknown;
473-
visitedTypes.delete(type);
474-
return result;
475-
}
476-
477-
const nonNullableType = checker.getNonNullableType(type);
478-
if (nonNullableType !== type) {
479-
const result = getTypeBooleanState(nonNullableType, checker, visitedTypes, functionTypesAreBoolean);
480-
visitedTypes.delete(type);
481-
return result;
482-
}
483-
484-
if (type.isUnion()) {
485-
const result = combineBooleanStates(
486-
type.types
487-
.filter(type => !isNullishType(type))
488-
.map(type => getTypeBooleanState(type, checker, visitedTypes, functionTypesAreBoolean)),
489-
);
490-
visitedTypes.delete(type);
491-
return result;
492-
}
493-
494-
const signatures = type.getCallSignatures();
495-
if (signatures.length > 0) {
496-
const result = functionTypesAreBoolean
497-
? combineBooleanStates(signatures.map(signature => getTypeBooleanState(signature.getReturnType(), checker, visitedTypes, false)))
498-
: nonBoolean;
499-
visitedTypes.delete(type);
500-
return result;
501-
}
502-
503-
const constraint = checker.getBaseConstraintOfType(type);
504-
if (constraint && constraint !== type) {
505-
const result = getTypeBooleanState(constraint, checker, visitedTypes, functionTypesAreBoolean);
506-
visitedTypes.delete(type);
507-
return result;
508-
}
509-
510-
if (type.getProperties().length === 0) {
511-
visitedTypes.delete(type);
512-
return unknown;
513-
}
514-
515-
const typeString = checker.typeToString(checker.getWidenedType(checker.getBaseTypeOfLiteralType(type)));
516-
visitedTypes.delete(type);
517-
518-
return typeString === 'boolean' ? boolean : nonBoolean;
519-
}
520-
521442
function getTypeInformationBooleanState(node, context, functionTypesAreBoolean = true) {
522443
const {parserServices} = context.sourceCode;
523444
if (!parserServices?.program) {
@@ -718,11 +639,6 @@ function isGlobalPromiseTypeAnnotation(node, scope) {
718639
return isGlobalPromiseTypeReference(node, scope);
719640
}
720641

721-
function getPromisedTypeBooleanState(type, checker) {
722-
const promisedType = checker.getPromisedTypeOfPromise(type);
723-
return promisedType ? getTypeBooleanState(promisedType, checker, new Set(), false) : unknown;
724-
}
725-
726642
function getAsyncFunctionTypeInformationBooleanState(node, context) {
727643
const {parserServices} = context.sourceCode;
728644
if (!parserServices?.program) {
@@ -1283,7 +1199,7 @@ function getAutofix({
12831199

12841200
/** @param {import('eslint').Rule.RuleContext} context */
12851201
const create = context => {
1286-
const {checkProperties, prefixes, ignore} = prepareOptions(context.options[0]);
1202+
const {checkProperties, prefixes, ignore, wrappers} = prepareOptions(context.options[0]);
12871203

12881204
if (prefixes.length === 0) {
12891205
return;
@@ -1300,8 +1216,18 @@ const create = context => {
13001216
const nameForPrefixCheck = getNameForPrefixCheck(variable, context);
13011217
const booleanPrefix = getBooleanPrefix(nameForPrefixCheck, prefixes);
13021218
if (booleanPrefix) {
1219+
const booleanState = getVariableBooleanState(variable, context);
1220+
const booleanWrapperState = wrappers.size === 0 || booleanState === boolean
1221+
? unknown
1222+
: getBooleanWrapperVariableState({
1223+
variable,
1224+
definition: getSupportedVariableDefinition(variable),
1225+
context,
1226+
wrappers,
1227+
});
1228+
const effectiveBooleanState = booleanWrapperState === unknown ? booleanState : booleanWrapperState;
13031229
if (
1304-
getVariableBooleanState(variable, context) === nonBoolean
1230+
effectiveBooleanState === nonBoolean
13051231
&& !isBooleanReactReferenceVariable(variable, context)
13061232
&& !isBooleanVueReferenceVariable(variable, context)
13071233
) {
@@ -1452,6 +1378,15 @@ const config = {
14521378
pattern: '^[a-z][a-zA-Z0-9]*$',
14531379
},
14541380
},
1381+
wrappers: {
1382+
type: 'object',
1383+
description: 'Wrapper type names and their boolean-like value members.',
1384+
additionalProperties: {
1385+
type: 'string',
1386+
description: 'The property or method that provides the wrapped value.',
1387+
minLength: 1,
1388+
},
1389+
},
14551390
ignore: {
14561391
type: 'array',
14571392
uniqueItems: true,
@@ -1460,7 +1395,7 @@ const config = {
14601395
},
14611396
},
14621397
],
1463-
defaultOptions: [{ignore: []}],
1398+
defaultOptions: [{ignore: [], wrappers: {}}],
14641399
messages,
14651400
languages: [
14661401
'js/js',
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import {combineBooleanStates, getPromisedTypeBooleanState, getTypeBooleanState} from './get-type-boolean-state.js';
2+
import {getBaseTypes, isTypeParameterType} from './types.js';
3+
4+
const boolean = 'boolean';
5+
const nonBoolean = 'non-boolean';
6+
const unknown = 'unknown';
7+
8+
// ObjectFlags.Mapped from TypeScript.
9+
const mappedTypeFlag = 32;
10+
11+
const isMappedType = type => (type.objectFlags % (mappedTypeFlag * 2)) >= mappedTypeFlag;
12+
13+
function getConfiguredMemberName(type, checker, wrappers, visitedTypes = new Set()) {
14+
if (!type) {
15+
return;
16+
}
17+
18+
const nonNullableType = checker.getNonNullableType(type);
19+
if (nonNullableType !== type) {
20+
return getConfiguredMemberName(nonNullableType, checker, wrappers, visitedTypes);
21+
}
22+
23+
if (visitedTypes.has(type)) {
24+
return;
25+
}
26+
27+
visitedTypes.add(type);
28+
29+
const typeNames = [
30+
type.symbol?.name,
31+
type.aliasSymbol?.name,
32+
type.target?.symbol?.name,
33+
];
34+
const memberName = typeNames
35+
.map(typeName => wrappers.get(typeName))
36+
.find(Boolean);
37+
if (memberName) {
38+
return memberName;
39+
}
40+
41+
if (isTypeParameterType(type)) {
42+
return getConfiguredMemberName(type.getConstraint(), checker, wrappers, visitedTypes);
43+
}
44+
45+
if (type.isIntersection()) {
46+
for (const constituentType of type.types) {
47+
const memberName = getConfiguredMemberName(constituentType, checker, wrappers, visitedTypes);
48+
if (memberName) {
49+
return memberName;
50+
}
51+
}
52+
}
53+
54+
if (isMappedType(type)) {
55+
for (const typeArgument of type.aliasTypeArguments ?? []) {
56+
const memberName = getConfiguredMemberName(typeArgument, checker, wrappers, visitedTypes);
57+
if (memberName) {
58+
return memberName;
59+
}
60+
}
61+
}
62+
63+
for (const baseType of getBaseTypes(type, checker)) {
64+
const memberName = getConfiguredMemberName(baseType, checker, wrappers, visitedTypes);
65+
if (memberName) {
66+
return memberName;
67+
}
68+
}
69+
}
70+
71+
function getBooleanValueState(type, checker) {
72+
const nonNullableType = checker.getNonNullableType(type);
73+
const typeState = getTypeBooleanState(nonNullableType, checker, new Set(), false);
74+
const promisedTypeState = getPromisedTypeBooleanState(nonNullableType, checker);
75+
76+
if (typeState === boolean || promisedTypeState === boolean) {
77+
return boolean;
78+
}
79+
80+
return typeState === nonBoolean || promisedTypeState === nonBoolean ? nonBoolean : unknown;
81+
}
82+
83+
function getBooleanWrapperMemberState(type, memberName, checker) {
84+
const member = checker.getPropertyOfType(type, memberName);
85+
if (!member) {
86+
return nonBoolean;
87+
}
88+
89+
const memberType = checker.getTypeOfPropertyOfType(type, memberName);
90+
if (!memberType) {
91+
return nonBoolean;
92+
}
93+
94+
const signatures = memberType.getCallSignatures();
95+
return signatures.length > 0
96+
? combineBooleanStates(signatures.map(signature => getBooleanValueState(signature.getReturnType(), checker)))
97+
: getBooleanValueState(memberType, checker);
98+
}
99+
100+
function getBooleanWrapperTypeState(type, checker, wrappers) {
101+
if (!type) {
102+
return unknown;
103+
}
104+
105+
const nonNullableType = checker.getNonNullableType(type);
106+
if (nonNullableType !== type) {
107+
return getBooleanWrapperTypeState(nonNullableType, checker, wrappers);
108+
}
109+
110+
if (type.isUnion()) {
111+
return combineBooleanStates(type.types.map(type => getBooleanWrapperTypeState(type, checker, wrappers)));
112+
}
113+
114+
const memberName = getConfiguredMemberName(type, checker, wrappers);
115+
if (!memberName) {
116+
return unknown;
117+
}
118+
119+
return getBooleanWrapperMemberState(type, memberName, checker);
120+
}
121+
122+
function getBooleanWrapperVariableState({variable, definition, context, wrappers}) {
123+
if (
124+
wrappers.size === 0
125+
|| !definition
126+
|| !['Variable', 'Parameter'].includes(definition.type)
127+
|| variable.references.some(reference => !reference.init && reference.isWrite())
128+
) {
129+
return unknown;
130+
}
131+
132+
const {parserServices} = context.sourceCode;
133+
if (!parserServices?.program) {
134+
return unknown;
135+
}
136+
137+
try {
138+
return getBooleanWrapperTypeState(
139+
parserServices.getTypeAtLocation(definition.name),
140+
parserServices.program.getTypeChecker(),
141+
wrappers,
142+
);
143+
} catch {
144+
return unknown;
145+
}
146+
}
147+
148+
export {getBooleanWrapperVariableState};

0 commit comments

Comments
 (0)