mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-15 06:39:17 +00:00
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:
1 parent
223e7855fe
commit
b846abc2a4
9 files changed
+273
-45
No files matched your search
@@ -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));
|
||||
}
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
const f = (): void => {};
|
||||
|
||||
declare namespace f {
|
||||
let property: number;
|
||||
}
|
||||
|
||||
f.property = 1;
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
export function f(): void;
|
||||
|
||||
/** @internal */
|
||||
export function f(): void {}
|
||||
|
||||
f.property = 1;
|
||||
+9
@@ -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;
|
||||
+26
@@ -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;
|
||||
: ^^^^^^^^^^
|
||||
`----
|
||||
|
||||
|
||||
```
|
||||
+23
@@ -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;
|
||||
: ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
`----
|
||||
|
||||
|
||||
```
|
||||
Reference in new issue
Block a user