Skip to content

Commit 46a77d0

Browse files
authored
fix(lint): detect misleading object returns for built-in class instances (#10141)
1 parent 343a5f6 commit 46a77d0

11 files changed

Lines changed: 329 additions & 1 deletion

File tree

.changeset/ripe-melons-rhyme.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Improved [`noUnnecessaryConditions`](https://biomejs.dev/linter/rules/no-unnecessary-conditions/) to detect conditions that are always truthy because they check built-in global class instances such as `Date`, `Map`, `Set`, `WeakMap`, and `Error`.

.changeset/sour-hounds-bow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Improved [`noMisleadingReturnType`](https://biomejs.dev/linter/rules/no-misleading-return-type/) to detect `object` return annotations that hide built-in global class instances such as `Date`, `Map`, `Set`, `WeakMap`, and `Error`.

crates/biome_js_analyze/src/lint/nursery/no_misleading_return_type.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,10 @@ fn is_nonunion_wider(annotated: &Type, inferred: &Type) -> bool {
927927
}
928928

929929
match (&*ann, &*inf) {
930+
(TypeData::ObjectKeyword, TypeData::InstanceOf(_)) => {
931+
found_wider = true;
932+
}
933+
930934
(TypeData::InstanceOf(ann_inst), TypeData::InstanceOf(inf_inst)) => {
931935
let same_base = match (ann.resolve(&ann_inst.ty), inf.resolve(&inf_inst.ty)) {
932936
(Some(a), Some(b)) => types_match(&a, &b),

crates/biome_js_analyze/tests/specs/nursery/noMisleadingReturnType/invalid.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,9 @@ class GetterClass { get code(): number { if (Math.random() > 0.5) return 200; re
6767
const getterObj = { get code(): number { if (Math.random() > 0.5) return 200; return 404; } };
6868

6969
class AsyncMethod { async getStatus(b: boolean): Promise<string> { if (b) return "loading"; return "idle"; } }
70+
71+
function dateObject(): object { return new Date(); }
72+
function mapObject(): object { return new Map(); }
73+
function setObject(): object { return new Set(); }
74+
function weakMapObject(): object { return new WeakMap(); }
75+
function errorObject(): object { return new Error(); }

crates/biome_js_analyze/tests/specs/nursery/noMisleadingReturnType/invalid.ts.snap

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ const getterObj = { get code(): number { if (Math.random() > 0.5) return 200; re
7474
7575
class AsyncMethod { async getStatus(b: boolean): Promise<string> { if (b) return "loading"; return "idle"; } }
7676
77+
function dateObject(): object { return new Date(); }
78+
function mapObject(): object { return new Map(); }
79+
function setObject(): object { return new Set(); }
80+
function weakMapObject(): object { return new WeakMap(); }
81+
function errorObject(): object { return new Error(); }
82+
7783
```
7884
7985
# Diagnostics
@@ -796,6 +802,7 @@ invalid.ts:69:48 lint/nursery/noMisleadingReturnType ━━━━━━━━━
796802
> 69 │ class AsyncMethod { async getStatus(b: boolean): Promise<string> { if (b) return "loading"; return "idle"; } }
797803
│ ^^^^^^^^^^^^^^^^^
798804
70 │
805+
71 │ function dateObject(): object { return new Date(); }
799806
800807
i A wider return type hides the precise types that callers could rely on.
801808
@@ -807,3 +814,116 @@ invalid.ts:69:48 lint/nursery/noMisleadingReturnType ━━━━━━━━━
807814
808815
809816
```
817+
818+
```
819+
invalid.ts:71:22 lint/nursery/noMisleadingReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
820+
821+
i The return type annotation is wider than what the function actually returns.
822+
823+
69 │ class AsyncMethod { async getStatus(b: boolean): Promise<string> { if (b) return "loading"; return "idle"; } }
824+
70 │
825+
> 71 │ function dateObject(): object { return new Date(); }
826+
│ ^^^^^^^^
827+
72 │ function mapObject(): object { return new Map(); }
828+
73 │ function setObject(): object { return new Set(); }
829+
830+
i A wider return type hides the precise types that callers could rely on.
831+
832+
i Narrow the return type to match what the function actually returns.
833+
834+
i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/9810 for more information or to report possible bugs.
835+
836+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
837+
838+
839+
```
840+
841+
```
842+
invalid.ts:72:21 lint/nursery/noMisleadingReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
843+
844+
i The return type annotation is wider than what the function actually returns.
845+
846+
71 │ function dateObject(): object { return new Date(); }
847+
> 72 │ function mapObject(): object { return new Map(); }
848+
│ ^^^^^^^^
849+
73 │ function setObject(): object { return new Set(); }
850+
74 │ function weakMapObject(): object { return new WeakMap(); }
851+
852+
i A wider return type hides the precise types that callers could rely on.
853+
854+
i Narrow the return type to match what the function actually returns.
855+
856+
i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/9810 for more information or to report possible bugs.
857+
858+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
859+
860+
861+
```
862+
863+
```
864+
invalid.ts:73:21 lint/nursery/noMisleadingReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
865+
866+
i The return type annotation is wider than what the function actually returns.
867+
868+
71 │ function dateObject(): object { return new Date(); }
869+
72 │ function mapObject(): object { return new Map(); }
870+
> 73 │ function setObject(): object { return new Set(); }
871+
│ ^^^^^^^^
872+
74 │ function weakMapObject(): object { return new WeakMap(); }
873+
75 │ function errorObject(): object { return new Error(); }
874+
875+
i A wider return type hides the precise types that callers could rely on.
876+
877+
i Narrow the return type to match what the function actually returns.
878+
879+
i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/9810 for more information or to report possible bugs.
880+
881+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
882+
883+
884+
```
885+
886+
```
887+
invalid.ts:74:25 lint/nursery/noMisleadingReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
888+
889+
i The return type annotation is wider than what the function actually returns.
890+
891+
72 │ function mapObject(): object { return new Map(); }
892+
73 │ function setObject(): object { return new Set(); }
893+
> 74 │ function weakMapObject(): object { return new WeakMap(); }
894+
│ ^^^^^^^^
895+
75 │ function errorObject(): object { return new Error(); }
896+
76 │
897+
898+
i A wider return type hides the precise types that callers could rely on.
899+
900+
i Narrow the return type to match what the function actually returns.
901+
902+
i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/9810 for more information or to report possible bugs.
903+
904+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
905+
906+
907+
```
908+
909+
```
910+
invalid.ts:75:23 lint/nursery/noMisleadingReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
911+
912+
i The return type annotation is wider than what the function actually returns.
913+
914+
73 │ function setObject(): object { return new Set(); }
915+
74 │ function weakMapObject(): object { return new WeakMap(); }
916+
> 75 │ function errorObject(): object { return new Error(); }
917+
│ ^^^^^^^^
918+
76 │
919+
920+
i A wider return type hides the precise types that callers could rely on.
921+
922+
i Narrow the return type to match what the function actually returns.
923+
924+
i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/9810 for more information or to report possible bugs.
925+
926+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
927+
928+
929+
```

crates/biome_js_analyze/tests/specs/nursery/noUnnecessaryConditions/invalid.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,6 @@ if (ro) console.log();
153153

154154
declare const partialObj2: Partial<{a: string}>;
155155
if (partialObj2) console.log();
156+
157+
const date = new Date();
158+
if (date) console.log(date);

crates/biome_js_analyze/tests/specs/nursery/noUnnecessaryConditions/invalid.ts.snap

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ if (ro) console.log();
160160
declare const partialObj2: Partial<{a: string}>;
161161
if (partialObj2) console.log();
162162
163+
const date = new Date();
164+
if (date) console.log(date);
165+
163166
```
164167
165168
# Diagnostics
@@ -884,6 +887,28 @@ invalid.ts:155:5 lint/nursery/noUnnecessaryConditions ━━━━━━━━
884887
> 155 │ if (partialObj2) console.log();
885888
│ ^^^^^^^^^^^
886889
156 │
890+
157 │ const date = new Date();
891+
892+
i The type being checked can never be falsy, making this condition redundant.
893+
894+
i Remove the condition.
895+
896+
i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/6611 for more information or to report possible bugs.
897+
898+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
899+
900+
901+
```
902+
903+
```
904+
invalid.ts:158:5 lint/nursery/noUnnecessaryConditions ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
905+
906+
! This condition is always truthy based on the type.
907+
908+
157 │ const date = new Date();
909+
> 158 │ if (date) console.log(date);
910+
│ ^^^^
911+
159 │
887912
888913
i The type being checked can never be falsy, making this condition redundant.
889914

crates/biome_js_type_info/src/globals.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,16 @@ pub fn global_type_name(id: TypeId) -> Option<&'static str> {
100100
DISPOSABLE_DISPOSE_ID => Some(DISPOSABLE_DISPOSE_ID_NAME),
101101
ASYNC_DISPOSABLE_ID => Some(ASYNC_DISPOSABLE_ID_NAME),
102102
ASYNC_DISPOSABLE_ASYNC_DISPOSE_ID => Some(ASYNC_DISPOSABLE_ASYNC_DISPOSE_ID_NAME),
103+
INSTANCEOF_DATE_ID => Some(INSTANCEOF_DATE_ID_NAME),
104+
DATE_ID => Some(DATE_ID_NAME),
105+
INSTANCEOF_MAP_ID => Some(INSTANCEOF_MAP_ID_NAME),
106+
MAP_ID => Some(MAP_ID_NAME),
107+
INSTANCEOF_SET_ID => Some(INSTANCEOF_SET_ID_NAME),
108+
SET_ID => Some(SET_ID_NAME),
109+
INSTANCEOF_WEAK_MAP_ID => Some(INSTANCEOF_WEAK_MAP_ID_NAME),
110+
WEAK_MAP_ID => Some(WEAK_MAP_ID_NAME),
111+
INSTANCEOF_ERROR_ID => Some(INSTANCEOF_ERROR_ID_NAME),
112+
ERROR_ID => Some(ERROR_ID_NAME),
103113
_ => None,
104114
}
105115
}
@@ -125,6 +135,16 @@ impl Default for GlobalsResolver {
125135
ty: ResolvedTypeId::new(TypeResolverLevel::Global, id).into(),
126136
};
127137

138+
let class = |name: &'static str, type_parameters: Box<[TypeReference]>| {
139+
TypeData::Class(Box::new(Class {
140+
name: Some(Text::new_static(name)),
141+
type_parameters,
142+
extends: None,
143+
implements: [].into(),
144+
members: Box::default(),
145+
}))
146+
};
147+
128148
let array_method_definition =
129149
|id: TypeId,
130150
param_type_id: TypeId,
@@ -414,6 +434,52 @@ impl Default for GlobalsResolver {
414434
return_type: ReturnType::Type(GLOBAL_INSTANCEOF_REGEXP_ID.into()),
415435
}),
416436
);
437+
builder.set_type_data(
438+
INSTANCEOF_DATE_ID,
439+
TypeData::instance_of(TypeReference::from(GLOBAL_DATE_ID)),
440+
);
441+
builder.set_type_data(DATE_ID, class(DATE_ID_NAME, Box::default()));
442+
builder.set_type_data(
443+
INSTANCEOF_MAP_ID,
444+
TypeData::instance_of(TypeReference::from(GLOBAL_MAP_ID)),
445+
);
446+
builder.set_type_data(
447+
MAP_ID,
448+
class(
449+
MAP_ID_NAME,
450+
Box::new([
451+
TypeReference::from(GLOBAL_T_ID),
452+
TypeReference::from(GLOBAL_U_ID),
453+
]),
454+
),
455+
);
456+
builder.set_type_data(
457+
INSTANCEOF_SET_ID,
458+
TypeData::instance_of(TypeReference::from(GLOBAL_SET_ID)),
459+
);
460+
builder.set_type_data(
461+
SET_ID,
462+
class(SET_ID_NAME, Box::new([TypeReference::from(GLOBAL_T_ID)])),
463+
);
464+
builder.set_type_data(
465+
INSTANCEOF_WEAK_MAP_ID,
466+
TypeData::instance_of(TypeReference::from(GLOBAL_WEAK_MAP_ID)),
467+
);
468+
builder.set_type_data(
469+
WEAK_MAP_ID,
470+
class(
471+
WEAK_MAP_ID_NAME,
472+
Box::new([
473+
TypeReference::from(GLOBAL_T_ID),
474+
TypeReference::from(GLOBAL_U_ID),
475+
]),
476+
),
477+
);
478+
builder.set_type_data(
479+
INSTANCEOF_ERROR_ID,
480+
TypeData::instance_of(TypeReference::from(GLOBAL_ERROR_ID)),
481+
);
482+
builder.set_type_data(ERROR_ID, class(ERROR_ID_NAME, Box::default()));
417483
// Known symbols
418484
builder.set_type_data(
419485
INSTANCEOF_SYMBOL_ID,
@@ -568,6 +634,16 @@ impl TypeResolver for GlobalsResolver {
568634
Some(GLOBAL_REGEXP_ID)
569635
} else if qualifier.is_symbol() && !qualifier.has_known_type_parameters() {
570636
Some(GLOBAL_SYMBOL_ID)
637+
} else if qualifier.is_date() && !qualifier.has_known_type_parameters() {
638+
Some(GLOBAL_DATE_ID)
639+
} else if qualifier.is_map() && !qualifier.has_known_type_parameters() {
640+
Some(GLOBAL_MAP_ID)
641+
} else if qualifier.is_set() && !qualifier.has_known_type_parameters() {
642+
Some(GLOBAL_SET_ID)
643+
} else if qualifier.is_weak_map() && !qualifier.has_known_type_parameters() {
644+
Some(GLOBAL_WEAK_MAP_ID)
645+
} else if qualifier.is_error() && !qualifier.has_known_type_parameters() {
646+
Some(GLOBAL_ERROR_ID)
571647
} else if qualifier.is_disposable() && !qualifier.has_known_type_parameters() {
572648
Some(GLOBAL_DISPOSABLE_ID)
573649
} else if qualifier.is_async_disposable() && !qualifier.has_known_type_parameters() {

crates/biome_js_type_info/src/globals_ids.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,10 +221,45 @@ define_global_type!(
221221
51,
222222
"AsyncDisposable[Symbol.asyncDispose]"
223223
);
224+
define_global_type!(
225+
INSTANCEOF_DATE_ID,
226+
INSTANCEOF_DATE_ID_NAME,
227+
52,
228+
"instanceof Date"
229+
);
230+
define_global_type!(DATE_ID, DATE_ID_NAME, 53, "Date");
231+
define_global_type!(
232+
INSTANCEOF_MAP_ID,
233+
INSTANCEOF_MAP_ID_NAME,
234+
54,
235+
"instanceof Map"
236+
);
237+
define_global_type!(MAP_ID, MAP_ID_NAME, 55, "Map");
238+
define_global_type!(
239+
INSTANCEOF_SET_ID,
240+
INSTANCEOF_SET_ID_NAME,
241+
56,
242+
"instanceof Set"
243+
);
244+
define_global_type!(SET_ID, SET_ID_NAME, 57, "Set");
245+
define_global_type!(
246+
INSTANCEOF_WEAK_MAP_ID,
247+
INSTANCEOF_WEAK_MAP_ID_NAME,
248+
58,
249+
"instanceof WeakMap"
250+
);
251+
define_global_type!(WEAK_MAP_ID, WEAK_MAP_ID_NAME, 59, "WeakMap");
252+
define_global_type!(
253+
INSTANCEOF_ERROR_ID,
254+
INSTANCEOF_ERROR_ID_NAME,
255+
60,
256+
"instanceof Error"
257+
);
258+
define_global_type!(ERROR_ID, ERROR_ID_NAME, 61, "Error");
224259

225260
/// Total number of predefined types.
226261
/// Must be one more than the highest TypeId above.
227-
pub const NUM_PREDEFINED_TYPES: usize = 52;
262+
pub const NUM_PREDEFINED_TYPES: usize = 62;
228263

229264
// Resolved type ID constants (TypeId wrapped with GlobalLevel)
230265
pub const GLOBAL_UNKNOWN_ID: ResolvedTypeId = ResolvedTypeId::new(GLOBAL_LEVEL, UNKNOWN_ID);
@@ -273,3 +308,8 @@ pub const GLOBAL_SYMBOL_ASYNC_DISPOSE_ID: ResolvedTypeId =
273308
pub const GLOBAL_DISPOSABLE_ID: ResolvedTypeId = ResolvedTypeId::new(GLOBAL_LEVEL, DISPOSABLE_ID);
274309
pub const GLOBAL_ASYNC_DISPOSABLE_ID: ResolvedTypeId =
275310
ResolvedTypeId::new(GLOBAL_LEVEL, ASYNC_DISPOSABLE_ID);
311+
pub const GLOBAL_DATE_ID: ResolvedTypeId = ResolvedTypeId::new(GLOBAL_LEVEL, DATE_ID);
312+
pub const GLOBAL_MAP_ID: ResolvedTypeId = ResolvedTypeId::new(GLOBAL_LEVEL, MAP_ID);
313+
pub const GLOBAL_SET_ID: ResolvedTypeId = ResolvedTypeId::new(GLOBAL_LEVEL, SET_ID);
314+
pub const GLOBAL_WEAK_MAP_ID: ResolvedTypeId = ResolvedTypeId::new(GLOBAL_LEVEL, WEAK_MAP_ID);
315+
pub const GLOBAL_ERROR_ID: ResolvedTypeId = ResolvedTypeId::new(GLOBAL_LEVEL, ERROR_ID);

0 commit comments

Comments
 (0)