fix(isolated-declarations): handle ambient expando properties (#25655)

## Summary

- treat value declarations in ambient namespaces as implicitly exported for expando-function validation
- preserve local versus exported namespace visibility to match TypeScript declaration merging
- add regression coverage for accepted ambient value declarations and rejected local or type-only declarations

## TypeScript compatibility

The regression fixture was checked with TypeScript 6.0.3. TypeScript and Oxc produce TS9023 only for the exported-function/local-namespace mismatch and the type-only namespace property.

Closes #24975
This commit is contained in:
camc314 committed 2026-08-17 02:25:03 +00:00
1 parent 223e7855fe
commit b846abc2a4
9 files changed
+273 -45

No files matched your search

+96 -45
View File
@@ -160,7 +160,7 @@ impl<'a> IsolatedDeclarations<'a> {
&mut self,
stmts: &ArenaVec<'a, Statement<'a>>,
) -> ArenaVec<'a, Statement<'a>> {
self.report_error_for_expando_function(stmts);
self.report_error_for_expando_function(stmts, true);
let mut stmts = stmts
.iter()
@@ -188,7 +188,7 @@ impl<'a> IsolatedDeclarations<'a> {
&mut self,
stmts: &ArenaVec<'a, Statement<'a>>,
) -> ArenaVec<'a, Statement<'a>> {
self.report_error_for_expando_function(stmts);
self.report_error_for_expando_function(stmts, false);
let mut stmts = stmts
.iter()
@@ -518,10 +518,13 @@ impl<'a> IsolatedDeclarations<'a> {
});
}
/// Collect exported names from a namespace declaration into `assignable_properties`.
/// Collect value declarations exported from a namespace into `assignable_properties`.
/// Ambient namespace members are implicitly exported, but the namespace's own export status
/// still determines which function declaration it can merge with.
fn collect_namespace_properties(
decl: &TSNamespaceDeclaration<'a>,
assignable_properties: &mut FxHashMap<&'a str, FxHashSet<Str<'a>>>,
is_exported: bool,
assignable_properties: &mut FxHashMap<(&'a str, bool), FxHashSet<Str<'a>>>,
) {
if decl.kind != TSNamespaceDeclarationKind::Namespace {
return;
@@ -529,37 +532,41 @@ impl<'a> IsolatedDeclarations<'a> {
let ident = &decl.id;
let TSNamespaceDeclarationBody::TSModuleBlock(block) = &decl.body else { return };
for stmt in &block.body {
let Statement::ExportDeclaration(decl) = stmt else { continue };
match &decl.declaration {
Declaration::VariableDeclaration(var) => {
let declaration = match stmt {
Statement::ExportDeclaration(decl) => Some(&decl.declaration),
_ if decl.declare => stmt.as_declaration(),
_ => None,
};
match declaration {
Some(Declaration::VariableDeclaration(var)) => {
for declarator in &var.declarations {
if let Some(name) = declarator.id.get_identifier_name() {
assignable_properties
.entry(ident.name.as_str())
.entry((ident.name.as_str(), is_exported))
.or_default()
.insert(name.into());
}
}
}
Declaration::FunctionDeclaration(func) => {
Some(Declaration::FunctionDeclaration(func)) => {
if let Some(name) = func.name() {
assignable_properties
.entry(ident.name.as_str())
.entry((ident.name.as_str(), is_exported))
.or_default()
.insert(name.into());
}
}
Declaration::ClassDeclaration(cls) => {
Some(Declaration::ClassDeclaration(cls)) => {
if let Some(id) = cls.id.as_ref() {
assignable_properties
.entry(ident.name.as_str())
.entry((ident.name.as_str(), is_exported))
.or_default()
.insert(id.name.into());
}
}
Declaration::TSEnumDeclaration(decl) => {
Some(Declaration::TSEnumDeclaration(decl)) => {
assignable_properties
.entry(ident.name.as_str())
.entry((ident.name.as_str(), is_exported))
.or_default()
.insert(decl.id.name.into());
}
@@ -568,32 +575,53 @@ impl<'a> IsolatedDeclarations<'a> {
}
}
fn report_error_for_expando_function(&self, stmts: &ArenaVec<'a, Statement<'a>>) {
let mut assignable_properties_for_namespace = FxHashMap::<&str, FxHashSet<Str>>::default();
fn report_error_for_expando_function(
&self,
stmts: &ArenaVec<'a, Statement<'a>>,
include_unexported: bool,
) {
let mut assignable_properties_for_namespace =
FxHashMap::<(&str, bool), FxHashSet<Str>>::default();
let mut can_expando_function_names = IdentHashSet::default();
let mut exported_expando_function_names = IdentHashSet::default();
let mut namespace_mergeable_function_names = IdentHashSet::default();
let mut emitted_function_overload_names = IdentHashSet::default();
for stmt in stmts {
let is_internal = self.has_internal_annotation(stmt.span());
match stmt {
Statement::ExportDeclaration(decl) => match &decl.declaration {
Declaration::FunctionDeclaration(func) => {
if func.body.is_some()
&& let Some(id) = func.id.as_ref()
{
can_expando_function_names.insert(id.name);
if let Some(name) = func.name() {
if func.body.is_none() {
if !is_internal {
emitted_function_overload_names.insert(name);
}
} else if !is_internal
|| emitted_function_overload_names.contains(&name)
{
can_expando_function_names.insert(name);
exported_expando_function_names.insert(name);
namespace_mergeable_function_names.insert(name);
}
}
}
Declaration::VariableDeclaration(decl) => {
for declarator in &decl.declarations {
if declarator.type_annotation.is_none()
&& declarator.init.as_ref().is_some_and(Expression::is_function)
&& let Some(name) = declarator.id.get_identifier_name()
{
can_expando_function_names.insert(name);
if !is_internal {
for declarator in &decl.declarations {
if declarator.type_annotation.is_none()
&& declarator.init.as_ref().is_some_and(Expression::is_function)
&& let Some(name) = declarator.id.get_identifier_name()
{
can_expando_function_names.insert(name);
exported_expando_function_names.insert(name);
}
}
}
}
Declaration::TSNamespaceDeclaration(decl) => {
Self::collect_namespace_properties(
decl,
true,
&mut assignable_properties_for_namespace,
);
}
@@ -602,34 +630,50 @@ impl<'a> IsolatedDeclarations<'a> {
Statement::ExportDefaultDeclaration(decl) => {
if let ExportDefaultDeclarationKind::FunctionDeclaration(func) =
&decl.declaration
&& func.body.is_some()
&& let Some(name) = func.name()
{
can_expando_function_names.insert(name);
if func.body.is_none() {
if !is_internal {
emitted_function_overload_names.insert(name);
}
} else if !is_internal || emitted_function_overload_names.contains(&name) {
can_expando_function_names.insert(name);
exported_expando_function_names.insert(name);
namespace_mergeable_function_names.insert(name);
}
}
}
Statement::FunctionDeclaration(func) => {
if func.body.is_some()
&& let Some(name) = func.name()
&& self.scope.has_value_reference(&name)
{
can_expando_function_names.insert(name);
if let Some(name) = func.name() {
if func.body.is_none() {
if !is_internal {
emitted_function_overload_names.insert(name);
}
} else if (!is_internal || emitted_function_overload_names.contains(&name))
&& (include_unexported || self.scope.has_value_reference(&name))
{
can_expando_function_names.insert(name);
namespace_mergeable_function_names.insert(name);
}
}
}
Statement::VariableDeclaration(decl) => {
for declarator in &decl.declarations {
if declarator.type_annotation.is_none()
&& declarator.init.as_ref().is_some_and(Expression::is_function)
&& let Some(name) = declarator.id.get_identifier_name()
&& self.scope.has_value_reference(&name)
{
can_expando_function_names.insert(name);
if !is_internal {
for declarator in &decl.declarations {
if declarator.type_annotation.is_none()
&& declarator.init.as_ref().is_some_and(Expression::is_function)
&& let Some(name) = declarator.id.get_identifier_name()
&& (include_unexported || self.scope.has_value_reference(&name))
{
can_expando_function_names.insert(name);
}
}
}
}
Statement::TSNamespaceDeclaration(decl) => {
Self::collect_namespace_properties(
decl,
false,
&mut assignable_properties_for_namespace,
);
}
@@ -639,11 +683,18 @@ impl<'a> IsolatedDeclarations<'a> {
&assignment.left
&& let Expression::Identifier(ident) = &static_member_expr.object
&& can_expando_function_names.contains(ident.name.as_str())
&& !assignable_properties_for_namespace
.get(ident.name.as_str())
.is_some_and(|properties| {
properties.contains(static_member_expr.property.name.as_str())
})
&& ![true, false].into_iter().any(|namespace_is_exported| {
(namespace_is_exported
|| (!exported_expando_function_names.contains(ident.name.as_str())
&& namespace_mergeable_function_names
.contains(ident.name.as_str())))
&& assignable_properties_for_namespace
.get(&(ident.name.as_str(), namespace_is_exported))
.is_some_and(|properties| {
properties
.contains(static_member_expr.property.name.as_str())
})
})
{
self.error(function_with_assigning_properties(static_member_expr.span));
}
@@ -0,0 +1,7 @@
const f = (): void => {};
declare namespace f {
let property: number;
}
f.property = 1;
@@ -0,0 +1,6 @@
export function f(): void;
/** @internal */
export function f(): void {}
f.property = 1;
@@ -0,0 +1,9 @@
/** @internal */
function functionDeclaration(): void {}
functionDeclaration.property = 1;
/** @internal */
const variableDeclaration = (): void => {};
variableDeclaration.property = 1;
@@ -0,0 +1,45 @@
export function exportedFunction(): void {}
export declare namespace exportedFunction {
let variable: number;
function func(): void;
class Class {}
enum Enum {}
}
exportedFunction.variable = 1;
exportedFunction.func = () => {};
exportedFunction.Class = class {};
exportedFunction.Enum = {} as typeof exportedFunction.Enum;
function localFunction(): void {}
declare namespace localFunction {
let property: number;
}
localFunction.property = 1;
export function exportedWithLocalNamespace(): void {}
declare namespace exportedWithLocalNamespace {
let property: number;
}
exportedWithLocalNamespace.property = 1;
export function exportedWithTypeOnlyProperty(): void {}
export declare namespace exportedWithTypeOnlyProperty {
interface property {}
}
exportedWithTypeOnlyProperty.property = {};
export default function defaultFunction(): void {}
declare namespace defaultFunction {
let property: number;
}
defaultFunction.property = 1;
@@ -0,0 +1,26 @@
---
source: crates/oxc_isolated_declarations/tests/mod.rs
input_file: crates/oxc_isolated_declarations/tests/fixtures/expando-function-ambient-variable.ts
---
```
==================== .D.TS ====================
declare const f: () => void;
declare namespace f {
let property: number;
}
==================== Errors ====================
x TS9023: Assigning properties to functions without declaring them is not
| supported with --isolatedDeclarations. Add an explicit declaration for the
| properties assigned to this function.
,-[7:1]
6 |
7 | f.property = 1;
: ^^^^^^^^^^
`----
```
@@ -0,0 +1,23 @@
---
source: crates/oxc_isolated_declarations/tests/mod.rs
input_file: crates/oxc_isolated_declarations/tests/fixtures/expando-function-strip-internal-overload.ts
---
```
==================== .D.TS ====================
export declare function f(): void;
==================== Errors ====================
x TS9023: Assigning properties to functions without declaring them is not
| supported with --isolatedDeclarations. Add an explicit declaration for the
| properties assigned to this function.
,-[6:1]
5 |
6 | f.property = 1;
: ^^^^^^^^^^
`----
```
@@ -0,0 +1,6 @@
---
source: crates/oxc_isolated_declarations/tests/mod.rs
input_file: crates/oxc_isolated_declarations/tests/fixtures/expando-function-strip-internal.ts
---
```
==================== .D.TS ====================
@@ -0,0 +1,55 @@
---
source: crates/oxc_isolated_declarations/tests/mod.rs
input_file: crates/oxc_isolated_declarations/tests/fixtures/issue-24975.ts
---
```
==================== .D.TS ====================
export declare function exportedFunction(): void;
export declare namespace exportedFunction {
let variable: number;
function func(): void;
class Class {}
enum Enum {}
}
export declare function exportedWithLocalNamespace(): void;
export declare function exportedWithTypeOnlyProperty(): void;
export declare namespace exportedWithTypeOnlyProperty {
interface property {}
}
export default function defaultFunction(): void;
==================== Errors ====================
x TS9023: Assigning properties to functions without declaring them is not
| supported with --isolatedDeclarations. Add an explicit declaration for the
| properties assigned to this function.
,-[29:1]
28 |
29 | exportedWithLocalNamespace.property = 1;
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
30 |
`----
x TS9023: Assigning properties to functions without declaring them is not
| supported with --isolatedDeclarations. Add an explicit declaration for the
| properties assigned to this function.
,-[37:1]
36 |
37 | exportedWithTypeOnlyProperty.property = {};
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
38 |
`----
x TS9023: Assigning properties to functions without declaring them is not
| supported with --isolatedDeclarations. Add an explicit declaration for the
| properties assigned to this function.
,-[45:1]
44 |
45 | defaultFunction.property = 1;
: ^^^^^^^^^^^^^^^^^^^^^^^^
`----
```