mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-14 22:29:17 +00:00
fix(isolated-declarations): preserve unresolved parameter types (#25909)
#25292 made isolated declaration emit report a false TS9025 for a defaulted parameter whose annotation is an indexed access type. This breaks Vite's `UserConfig["base"]` case when Rolldown upgrades Oxc, even though the indexed property already includes `undefined`. Oxc's isolated declaration emitter has no type checker, so it cannot safely resolve indexed accesses, type references, conditional types, or similar annotations. This change uses a syntax-only classification: widen or diagnose types whose syntax is conclusive, and preserve checker-dependent types rather than emit an unstable error. The union/intersection rules retain the distinct behavior of `any`, `never`, `unknown`, and `void`, and an explicit `undefined` union member is never duplicated. Regression coverage includes the Rolldown/Vite case and a compact matrix of the relevant type algebra. The implementation and tests were reduced to one commit after review. Validated with the isolated-declarations tests, strict Clippy, the all-features workspace suite, lint, docs, AST generation, and TypeScript 5.6.1-rc/6.0.3 type-algebra checks. The full local `just ready` run stopped only at a runner-dependent source-map snapshot because the repository expects Node 26.5.0 and this machine has 25.8.1; the remaining suite passed with that and the known terminal-color snapshot group skipped. Follow-up to #25292. Related Rolldown failure: https://github.com/rolldown/rolldown/actions/runs/32158377691/job/95782148042?pr=10707 Disclosure: this change was prepared and verified with AI assistance (Codex).
This commit is contained in:
1 parent
83cb5163c0
commit
134fb2228d
6 files changed
+334
-70
No files matched your search
@@ -84,23 +84,36 @@ impl<'a> IsolatedDeclarations<'a> {
|
||||
new_type
|
||||
})
|
||||
.map(|ts_type| {
|
||||
// jf next param is not optional and current param is assignment pattern
|
||||
// we need to add undefined to it's type
|
||||
// If a defaulted parameter is followed by a required parameter, declaration
|
||||
// emit may need to add `undefined` to its type because the parameter cannot be
|
||||
// marked optional. Preserve annotations whose resolved type is unknown: unlike
|
||||
// TypeScript, isolated declaration emit does not have a type checker available
|
||||
// to determine whether they already include `undefined`.
|
||||
if (is_remaining_params_have_required
|
||||
|| (param.optional && param.has_modifier()))
|
||||
&& !contains_undefined(&ts_type)
|
||||
&& !has_explicit_undefined_union_member(&ts_type)
|
||||
{
|
||||
if can_add_undefined(&ts_type) {
|
||||
let undefined = TSType::new_ts_undefined_keyword(SPAN, self);
|
||||
let ts_type = if let TSType::TSUnionType(mut union) = ts_type {
|
||||
union.types.push(undefined);
|
||||
TSType::TSUnionType(union)
|
||||
} else {
|
||||
TSType::new_ts_union_type(SPAN, [ts_type, undefined], self)
|
||||
};
|
||||
return TSTypeAnnotation::boxed(SPAN, ts_type, self);
|
||||
let presence = undefined_presence(&ts_type);
|
||||
let can_add = can_add_undefined(&ts_type);
|
||||
match (presence, can_add) {
|
||||
(UndefinedPresence::Present, _)
|
||||
| (UndefinedPresence::Unresolved, false) => {}
|
||||
(_, true) => {
|
||||
// Adding `undefined` is either required or redundant after
|
||||
// TypeScript resolves the type.
|
||||
let undefined = TSType::new_ts_undefined_keyword(SPAN, self);
|
||||
let ts_type = if let TSType::TSUnionType(mut union) = ts_type {
|
||||
union.types.push(undefined);
|
||||
TSType::TSUnionType(union)
|
||||
} else {
|
||||
TSType::new_ts_union_type(SPAN, [ts_type, undefined], self)
|
||||
};
|
||||
return TSTypeAnnotation::boxed(SPAN, ts_type, self);
|
||||
}
|
||||
(_, false) => {
|
||||
self.error(implicitly_adding_undefined_to_type(param.span));
|
||||
}
|
||||
}
|
||||
self.error(implicitly_adding_undefined_to_type(param.span));
|
||||
}
|
||||
|
||||
TSTypeAnnotation::boxed(SPAN, ts_type, self)
|
||||
@@ -188,13 +201,121 @@ pub fn get_function_span(func: &Function<'_>) -> Span {
|
||||
func.id.as_ref().map_or_else(|| Span::empty(func.params.span.start), |id| id.span)
|
||||
}
|
||||
|
||||
fn contains_undefined(ts_type: &TSType<'_>) -> bool {
|
||||
/// Syntax-only categories that remain distinct while folding unions and intersections.
|
||||
#[derive(Clone, Copy)]
|
||||
enum UndefinedPresence {
|
||||
Present,
|
||||
Absent,
|
||||
/// `any` accepts `undefined` on its own but makes an intersection checker-dependent.
|
||||
Any,
|
||||
/// `never` excludes `undefined` and absorbs every other intersection operand.
|
||||
Never,
|
||||
/// `unknown` behaves like `Absent` for declaration emit but is the identity element of an
|
||||
/// intersection.
|
||||
UnknownKeyword,
|
||||
/// `void` excludes `undefined` on its own but retains it when intersected with `undefined`.
|
||||
Void,
|
||||
/// Resolving this annotation requires checker information.
|
||||
Unresolved,
|
||||
}
|
||||
|
||||
impl UndefinedPresence {
|
||||
/// Combine the classifications of two union members.
|
||||
fn union(self, other: Self) -> Self {
|
||||
use UndefinedPresence::{Absent, Any, Never, Present, UnknownKeyword, Unresolved, Void};
|
||||
match (self, other) {
|
||||
(Unresolved, _) | (_, Unresolved) => Unresolved,
|
||||
(Any, _) | (_, Any) => Any,
|
||||
(UnknownKeyword, _) | (_, UnknownKeyword) => UnknownKeyword,
|
||||
(Present, _) | (_, Present) => Present,
|
||||
(Void, _) | (_, Void) => Void,
|
||||
(Absent, _) | (_, Absent) => Absent,
|
||||
(Never, Never) => Never,
|
||||
}
|
||||
}
|
||||
|
||||
/// Combine the classifications of two intersection members.
|
||||
fn intersection(self, other: Self) -> Self {
|
||||
use UndefinedPresence::{Absent, Any, Never, Present, UnknownKeyword, Unresolved, Void};
|
||||
match (self, other) {
|
||||
(Never, _) | (_, Never) => Never,
|
||||
(Unresolved | Any, _) | (_, Unresolved | Any) => Unresolved,
|
||||
(Absent, _) | (_, Absent) => Absent,
|
||||
(Present, _) | (_, Present) => Present,
|
||||
(Void, _) | (_, Void) => Void,
|
||||
(UnknownKeyword, UnknownKeyword) => UnknownKeyword,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Classify whether annotation syntax proves that the parameter type includes `undefined`.
|
||||
fn undefined_presence(ts_type: &TSType<'_>) -> UndefinedPresence {
|
||||
match ts_type {
|
||||
TSType::TSUndefinedKeyword(_) => UndefinedPresence::Present,
|
||||
TSType::TSAnyKeyword(_) => UndefinedPresence::Any,
|
||||
TSType::TSUnknownKeyword(_) => UndefinedPresence::UnknownKeyword,
|
||||
TSType::TSBigIntKeyword(_)
|
||||
| TSType::TSBooleanKeyword(_)
|
||||
| TSType::TSIntrinsicKeyword(_)
|
||||
| TSType::TSNullKeyword(_)
|
||||
| TSType::TSNumberKeyword(_)
|
||||
| TSType::TSObjectKeyword(_)
|
||||
| TSType::TSStringKeyword(_)
|
||||
| TSType::TSSymbolKeyword(_)
|
||||
| TSType::TSLiteralType(_)
|
||||
| TSType::TSFunctionType(_)
|
||||
| TSType::TSConstructorType(_)
|
||||
| TSType::TSArrayType(_)
|
||||
| TSType::TSTupleType(_)
|
||||
| TSType::TSMappedType(_)
|
||||
| TSType::TSTypeLiteral(_)
|
||||
| TSType::TSTypeOperatorType(_)
|
||||
| TSType::TSTemplateLiteralType(_)
|
||||
| TSType::TSThisType(_) => UndefinedPresence::Absent,
|
||||
TSType::TSNeverKeyword(_) => UndefinedPresence::Never,
|
||||
TSType::TSVoidKeyword(_) => UndefinedPresence::Void,
|
||||
TSType::TSParenthesizedType(parenthesized) => {
|
||||
undefined_presence(&parenthesized.type_annotation)
|
||||
}
|
||||
TSType::TSUnionType(union) => union
|
||||
.types
|
||||
.iter()
|
||||
.map(undefined_presence)
|
||||
.fold(UndefinedPresence::Never, UndefinedPresence::union),
|
||||
// Resolving these types requires type information. Treat them as unknown so declaration
|
||||
// emit does not report a false-positive TS9025 for a type that already contains
|
||||
// `undefined`. Some variants cannot occur directly as a parameter annotation, but listing
|
||||
// them keeps this classification exhaustive as new `TSType` variants are added.
|
||||
TSType::TSConditionalType(_)
|
||||
| TSType::TSImportType(_)
|
||||
| TSType::TSIndexedAccessType(_)
|
||||
| TSType::TSInferType(_)
|
||||
| TSType::TSNamedTupleMember(_)
|
||||
| TSType::TSTypePredicate(_)
|
||||
| TSType::TSTypeQuery(_)
|
||||
| TSType::TSTypeReference(_)
|
||||
| TSType::JSDocNullableType(_)
|
||||
| TSType::JSDocNonNullableType(_)
|
||||
| TSType::JSDocUnknownType(_) => UndefinedPresence::Unresolved,
|
||||
TSType::TSIntersectionType(intersection) => intersection
|
||||
.types
|
||||
.iter()
|
||||
.map(undefined_presence)
|
||||
.fold(UndefinedPresence::UnknownKeyword, UndefinedPresence::intersection),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the annotation itself already has an explicit `undefined` union member.
|
||||
///
|
||||
/// Do not descend through intersections: `undefined & string` excludes `undefined`, whereas a
|
||||
/// parenthesized or nested union member still spells it explicitly.
|
||||
fn has_explicit_undefined_union_member(ts_type: &TSType<'_>) -> bool {
|
||||
match ts_type {
|
||||
TSType::TSUndefinedKeyword(_) => true,
|
||||
TSType::TSUnionType(union) => union.types.iter().any(contains_undefined),
|
||||
TSType::TSParenthesizedType(parenthesized) => {
|
||||
contains_undefined(&parenthesized.type_annotation)
|
||||
has_explicit_undefined_union_member(&parenthesized.type_annotation)
|
||||
}
|
||||
TSType::TSUnionType(union) => union.types.iter().any(has_explicit_undefined_union_member),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
export interface UserConfig {
|
||||
base?: string | false;
|
||||
}
|
||||
|
||||
declare const configDefaults: Required<UserConfig>;
|
||||
|
||||
export type Base = UserConfig["base"];
|
||||
|
||||
export function resolveBaseUrl(
|
||||
base: UserConfig["base"] = configDefaults.base,
|
||||
isBuild: boolean,
|
||||
): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
export function resolveAliasedBaseUrl(
|
||||
base: Base = configDefaults.base,
|
||||
isBuild: boolean,
|
||||
): string {
|
||||
return "";
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
interface Source {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function mapped(
|
||||
value: { [K in keyof Source]: Source[K] } = { value: "" },
|
||||
required: string,
|
||||
): void {}
|
||||
|
||||
export function keyOf(
|
||||
value: keyof Source = "value",
|
||||
required: string,
|
||||
): void {}
|
||||
|
||||
export function readonlyTuple(
|
||||
value: readonly [string] = [""],
|
||||
required: string,
|
||||
): void {}
|
||||
|
||||
export function intersection(
|
||||
value: { optional?: string } & number[] = [],
|
||||
required: string,
|
||||
): void {}
|
||||
|
||||
export function operatorIntersection(
|
||||
value: (keyof Source) & string = "value",
|
||||
required: string,
|
||||
): void {}
|
||||
|
||||
export function unknownOperatorIntersection(
|
||||
value: unknown & keyof Source = "value",
|
||||
required: string,
|
||||
): void {}
|
||||
|
||||
export function unresolvedIntersection<T>(
|
||||
value: T & string = "" as T & string,
|
||||
required: string,
|
||||
): void {}
|
||||
|
||||
export function allUnknownIntersection(
|
||||
value: unknown & unknown = undefined,
|
||||
required: string,
|
||||
): void {}
|
||||
|
||||
export function unresolvedUndefinedUnionIntersection<T>(
|
||||
value: (T | undefined) & string = "" as (T | undefined) & string,
|
||||
required: string,
|
||||
): void {}
|
||||
|
||||
export function unknownUndefinedUnionOperatorIntersection(
|
||||
value: (unknown | undefined) & keyof Source = "value",
|
||||
required: string,
|
||||
): void {}
|
||||
|
||||
export function anyUndefinedUnionOperatorIntersection(
|
||||
value: (any | undefined) & keyof Source = "value",
|
||||
required: string,
|
||||
): void {}
|
||||
|
||||
export function unresolvedNeverOperatorIntersection<T>(
|
||||
value: T & never & keyof Source = undefined as never,
|
||||
required: string,
|
||||
): void {}
|
||||
|
||||
export function voidUnionUndefinedIntersection(
|
||||
value: (void | string) & undefined = undefined,
|
||||
required: string,
|
||||
): void {}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
---
|
||||
source: crates/oxc_isolated_declarations/tests/mod.rs
|
||||
input_file: crates/oxc_isolated_declarations/tests/fixtures/default-parameter-indexed-access.ts
|
||||
---
|
||||
```
|
||||
==================== .D.TS ====================
|
||||
|
||||
export interface UserConfig {
|
||||
base?: string | false;
|
||||
}
|
||||
export type Base = UserConfig["base"];
|
||||
export declare function resolveBaseUrl(base: UserConfig["base"], isBuild: boolean): string;
|
||||
export declare function resolveAliasedBaseUrl(base: Base, isBuild: boolean): string;
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
source: crates/oxc_isolated_declarations/tests/mod.rs
|
||||
input_file: crates/oxc_isolated_declarations/tests/fixtures/default-parameter-type-kinds.ts
|
||||
---
|
||||
```
|
||||
==================== .D.TS ====================
|
||||
|
||||
interface Source {
|
||||
value: string;
|
||||
}
|
||||
export declare function mapped(value: { [K in keyof Source]: Source[K] }, required: string): void;
|
||||
export declare function keyOf(value: keyof Source, required: string): void;
|
||||
export declare function readonlyTuple(value: readonly [string], required: string): void;
|
||||
export declare function intersection(value: {
|
||||
optional?: string;
|
||||
} & number[] | undefined, required: string): void;
|
||||
export declare function operatorIntersection(value: (keyof Source) & string, required: string): void;
|
||||
export declare function unknownOperatorIntersection(value: unknown & keyof Source, required: string): void;
|
||||
export declare function unresolvedIntersection<T>(value: T & string, required: string): void;
|
||||
export declare function allUnknownIntersection(value: unknown & unknown | undefined, required: string): void;
|
||||
export declare function unresolvedUndefinedUnionIntersection<T>(value: (T | undefined) & string, required: string): void;
|
||||
export declare function unknownUndefinedUnionOperatorIntersection(value: (unknown | undefined) & keyof Source, required: string): void;
|
||||
export declare function anyUndefinedUnionOperatorIntersection(value: (any | undefined) & keyof Source, required: string): void;
|
||||
export declare function unresolvedNeverOperatorIntersection<T>(value: T & never & keyof Source, required: string): void;
|
||||
export declare function voidUnionUndefinedIntersection(value: (void | string) & undefined, required: string): void;
|
||||
export {};
|
||||
|
||||
|
||||
==================== Errors ====================
|
||||
|
||||
x TS9025: Declaration emit for this parameter requires implicitly adding
|
||||
| undefined to it's type. This is not supported with --isolatedDeclarations.
|
||||
,-[6:3]
|
||||
5 | export function mapped(
|
||||
6 | value: { [K in keyof Source]: Source[K] } = { value: "" },
|
||||
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
7 | required: string,
|
||||
`----
|
||||
|
||||
x TS9025: Declaration emit for this parameter requires implicitly adding
|
||||
| undefined to it's type. This is not supported with --isolatedDeclarations.
|
||||
,-[11:3]
|
||||
10 | export function keyOf(
|
||||
11 | value: keyof Source = "value",
|
||||
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
12 | required: string,
|
||||
`----
|
||||
|
||||
x TS9025: Declaration emit for this parameter requires implicitly adding
|
||||
| undefined to it's type. This is not supported with --isolatedDeclarations.
|
||||
,-[16:3]
|
||||
15 | export function readonlyTuple(
|
||||
16 | value: readonly [string] = [""],
|
||||
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
17 | required: string,
|
||||
`----
|
||||
|
||||
x TS9025: Declaration emit for this parameter requires implicitly adding
|
||||
| undefined to it's type. This is not supported with --isolatedDeclarations.
|
||||
,-[26:3]
|
||||
25 | export function operatorIntersection(
|
||||
26 | value: (keyof Source) & string = "value",
|
||||
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
27 | required: string,
|
||||
`----
|
||||
|
||||
x TS9025: Declaration emit for this parameter requires implicitly adding
|
||||
| undefined to it's type. This is not supported with --isolatedDeclarations.
|
||||
,-[31:3]
|
||||
30 | export function unknownOperatorIntersection(
|
||||
31 | value: unknown & keyof Source = "value",
|
||||
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
32 | required: string,
|
||||
`----
|
||||
|
||||
x TS9025: Declaration emit for this parameter requires implicitly adding
|
||||
| undefined to it's type. This is not supported with --isolatedDeclarations.
|
||||
,-[51:3]
|
||||
50 | export function unknownUndefinedUnionOperatorIntersection(
|
||||
51 | value: (unknown | undefined) & keyof Source = "value",
|
||||
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
52 | required: string,
|
||||
`----
|
||||
|
||||
x TS9025: Declaration emit for this parameter requires implicitly adding
|
||||
| undefined to it's type. This is not supported with --isolatedDeclarations.
|
||||
,-[61:3]
|
||||
60 | export function unresolvedNeverOperatorIntersection<T>(
|
||||
61 | value: T & never & keyof Source = undefined as never,
|
||||
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
62 | required: string,
|
||||
`----
|
||||
|
||||
|
||||
```
|
||||
@@ -37,42 +37,6 @@ export {};
|
||||
|
||||
==================== Errors ====================
|
||||
|
||||
x TS9025: Declaration emit for this parameter requires implicitly adding
|
||||
| undefined to it's type. This is not supported with --isolatedDeclarations.
|
||||
,-[18:30]
|
||||
17 | // Incorrect
|
||||
18 | export function fnDeclBad<T>(p: T = [], rParam: T = "", r2: T): void { }
|
||||
: ^^^^^^^^^
|
||||
19 | export function fnDeclBad2<T>(p: T = [], r2: T): void { }
|
||||
`----
|
||||
|
||||
x TS9025: Declaration emit for this parameter requires implicitly adding
|
||||
| undefined to it's type. This is not supported with --isolatedDeclarations.
|
||||
,-[18:41]
|
||||
17 | // Incorrect
|
||||
18 | export function fnDeclBad<T>(p: T = [], rParam: T = "", r2: T): void { }
|
||||
: ^^^^^^^^^^^^^^
|
||||
19 | export function fnDeclBad2<T>(p: T = [], r2: T): void { }
|
||||
`----
|
||||
|
||||
x TS9025: Declaration emit for this parameter requires implicitly adding
|
||||
| undefined to it's type. This is not supported with --isolatedDeclarations.
|
||||
,-[19:31]
|
||||
18 | export function fnDeclBad<T>(p: T = [], rParam: T = "", r2: T): void { }
|
||||
19 | export function fnDeclBad2<T>(p: T = [], r2: T): void { }
|
||||
: ^^^^^^^^^
|
||||
20 | export function fnDeclBad3<T>(p: T = [], r2: T, rParam?: T): void { }
|
||||
`----
|
||||
|
||||
x TS9025: Declaration emit for this parameter requires implicitly adding
|
||||
| undefined to it's type. This is not supported with --isolatedDeclarations.
|
||||
,-[20:31]
|
||||
19 | export function fnDeclBad2<T>(p: T = [], r2: T): void { }
|
||||
20 | export function fnDeclBad3<T>(p: T = [], r2: T, rParam?: T): void { }
|
||||
: ^^^^^^^^^
|
||||
21 |
|
||||
`----
|
||||
|
||||
x TS9011: Parameter must have an explicit type annotation with
|
||||
| --isolatedDeclarations.
|
||||
,-[22:24]
|
||||
@@ -91,15 +55,6 @@ export {};
|
||||
27 | return 2;
|
||||
`----
|
||||
|
||||
x TS9025: Declaration emit for this parameter requires implicitly adding
|
||||
| undefined to it's type. This is not supported with --isolatedDeclarations.
|
||||
,-[36:43]
|
||||
35 | type Foo = {};
|
||||
36 | export function withAnyTypeReferenceUnion(a: any | Foo = {}, b: string): void { }
|
||||
: ^^^^^^^^^^^^^^^^^
|
||||
37 | export function withAnyTypeReferenceUndefinedUnion(a: any | Foo | undefined = {}, b: string): void { }
|
||||
`----
|
||||
|
||||
x TS9025: Declaration emit for this parameter requires implicitly adding
|
||||
| undefined to it's type. This is not supported with --isolatedDeclarations.
|
||||
,-[38:42]
|
||||
@@ -109,14 +64,5 @@ export {};
|
||||
39 | export function withAnyIntersectionTypeReferenceUnion(a: any | (Foo & {}) = {}, b: string): void { }
|
||||
`----
|
||||
|
||||
x TS9025: Declaration emit for this parameter requires implicitly adding
|
||||
| undefined to it's type. This is not supported with --isolatedDeclarations.
|
||||
,-[39:55]
|
||||
38 | export function withAnyTypeOperatorUnion(a: any | keyof {} = 1, b: string): void { }
|
||||
39 | export function withAnyIntersectionTypeReferenceUnion(a: any | (Foo & {}) = {}, b: string): void { }
|
||||
: ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
40 | export function withUnknown(a: unknown = 1, b: string): void { }
|
||||
`----
|
||||
|
||||
|
||||
```
|
||||
Reference in new issue
Block a user