Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 104 additions & 8 deletions crates/oxc_angular_compiler/src/class_metadata/builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
use oxc_allocator::{Allocator, Box, Vec as AllocVec};
use oxc_ast::ast::{
BindingPattern, ChainElement, Class, ClassElement, Declaration, Decorator, Expression,
FormalParameter, ObjectPropertyKind, PropertyKey, Statement, TSNamespaceDeclarationBody,
TSType, TSTypeName, VariableDeclaration,
ForStatementInit, ForStatementLeft, FormalParameter, ObjectPropertyKind, PropertyKey,
Statement, TSNamespaceDeclarationBody, TSType, TSTypeName, VariableDeclaration,
};
use oxc_span::GetSpan;
use oxc_str::Ident;
Expand Down Expand Up @@ -448,8 +448,7 @@ pub fn build_ctor_params_metadata_in<'a>(
// (issue #563). `consts` carries the program for every in-file call site;
// without it these checks are skipped.
let class_type_params = type_param_names_of(class);
let local_decls =
consts.and_then(StringConsts::program).map(|program| LocalDecls::collect(&program.body));
let local_decls = consts.and_then(StringConsts::local_decls);

let mut param_entries = AllocVec::new_in(&allocator);

Expand All @@ -464,7 +463,7 @@ pub fn build_ctor_params_metadata_in<'a>(
namespace_registry,
import_map,
&class_type_params,
local_decls.as_ref(),
local_decls,
)
.unwrap_or_else(|| {
OutputExpression::Literal(Box::new_in(
Expand Down Expand Up @@ -1146,21 +1145,24 @@ pub fn type_param_names_of<'a>(class: &Class<'a>) -> FxHashSet<Ident<'a>> {
/// `ns.Interface` lookups and for the merged `interface I` + `namespace I`
/// case — the merged symbol's valueDeclaration is the namespace, so it emits.
#[derive(Default)]
struct LocalDecls<'a> {
pub(crate) struct LocalDecls<'a> {
type_decls: FxHashSet<&'a str>,
/// Names with a runtime declaration — classes, enums, functions, vars,
/// `import X =` aliases. An interface or alias merged with one of these
/// resolves to a symbol whose valueDeclaration is the value declaration,
/// so `type:` emits it (ngtsc: `class Foo` + `interface Foo` → `Foo`).
value_decls: FxHashSet<&'a str>,
namespaces: FxHashMap<&'a str, LocalDecls<'a>>,
/// Names a `declare global` block declares (root only). See
/// [`Self::is_type_only_token`].
global_names: FxHashSet<&'a str>,
}

impl<'a> LocalDecls<'a> {
/// Collect declarations from a statement list, unwrapping `export` and
/// hoisting `declare global` bodies to file scope like ngtsc's program
/// scope — wherever the `declare global` block is nested.
fn collect(stmts: &'a [Statement<'a>]) -> Self {
pub(crate) fn collect(stmts: &'a [Statement<'a>]) -> Self {
let mut decls = LocalDecls::default();
// `declare global` inners bind at file scope, so they are queued and
// inserted into the root `decls` rather than whichever namespace-level
Expand All @@ -1172,6 +1174,11 @@ impl<'a> LocalDecls<'a> {
let stmt = globals[i];
i += 1;
decls.insert_statement(stmt, &mut globals);
let mut global = LocalDecls::default();
global.insert_statement(stmt, &mut std::vec::Vec::new());
decls.global_names.extend(global.type_decls);
decls.global_names.extend(global.value_decls);
decls.global_names.extend(global.namespaces.into_keys());
}
decls
}
Expand Down Expand Up @@ -1248,10 +1255,72 @@ impl<'a> LocalDecls<'a> {
Statement::TSGlobalDeclaration(global) => {
globals.extend(global.body.body.iter());
}
_ => self.insert_hoisted_vars(stmt),
}
}

/// The `var`s of a block or loop: they bind in the enclosing scope, so
/// `{ var Foo = class {} }` merges with `interface Foo` (a value). Function
/// and class bodies have their own scope.
fn insert_hoisted_vars(&mut self, stmt: &'a Statement<'a>) {
match stmt {
Statement::VariableDeclaration(decl) if decl.kind.is_var() => {
self.insert_var_names(decl);
}
Statement::BlockStatement(block) => {
block.body.iter().for_each(|stmt| self.insert_hoisted_vars(stmt));
}
Statement::IfStatement(stmt) => {
self.insert_hoisted_vars(&stmt.consequent);
if let Some(alternate) = &stmt.alternate {
self.insert_hoisted_vars(alternate);
}
}
Statement::ForStatement(stmt) => {
if let Some(ForStatementInit::VariableDeclaration(decl)) = &stmt.init
&& decl.kind.is_var()
{
self.insert_var_names(decl);
}
self.insert_hoisted_vars(&stmt.body);
}
Statement::ForInStatement(stmt) => {
self.insert_loop_left(&stmt.left);
self.insert_hoisted_vars(&stmt.body);
}
Statement::ForOfStatement(stmt) => {
self.insert_loop_left(&stmt.left);
self.insert_hoisted_vars(&stmt.body);
}
Statement::WhileStatement(stmt) => self.insert_hoisted_vars(&stmt.body),
Statement::DoWhileStatement(stmt) => self.insert_hoisted_vars(&stmt.body),
Statement::LabeledStatement(stmt) => self.insert_hoisted_vars(&stmt.body),
Statement::TryStatement(stmt) => {
stmt.block.body.iter().for_each(|stmt| self.insert_hoisted_vars(stmt));
if let Some(handler) = &stmt.handler {
handler.body.body.iter().for_each(|stmt| self.insert_hoisted_vars(stmt));
}
if let Some(finalizer) = &stmt.finalizer {
finalizer.body.iter().for_each(|stmt| self.insert_hoisted_vars(stmt));
}
}
Statement::SwitchStatement(stmt) => {
for case in &stmt.cases {
case.consequent.iter().for_each(|stmt| self.insert_hoisted_vars(stmt));
}
}
_ => {}
}
}

fn insert_loop_left(&mut self, left: &'a ForStatementLeft<'a>) {
if let ForStatementLeft::VariableDeclaration(decl) = left
&& decl.kind.is_var()
{
self.insert_var_names(decl);
}
}

fn insert_declaration(
&mut self,
decl: &'a Declaration<'a>,
Expand Down Expand Up @@ -1374,7 +1443,7 @@ impl<'a> LocalDecls<'a> {
/// declaration (interface or type alias) that no same-named namespace
/// or value declaration merges with. Only that case is `UNAVAILABLE` in
/// local compilation; everything else emits as written.
fn is_type_only(&self, path: &[Ident<'_>]) -> bool {
pub(crate) fn is_type_only(&self, path: &[Ident<'_>]) -> bool {
match path.split_first() {
None => false,
// A merged `interface I` + `namespace I` or `interface I` +
Expand All @@ -1390,6 +1459,33 @@ impl<'a> LocalDecls<'a> {
}
}
}

/// [`Self::is_type_only`] for a DI token of `class`'s constructor, leaving
/// out names one file can't prove have no value:
/// - the class's own name: a class nested in a block or function, named
/// like a top-level interface, is what its parameter types read;
/// - a name a `declare global` block declares: it merges with values other
/// files declare (lib.dom's `declare var Window`);
/// - a member of a namespace merged with a value: a class's statics can
/// hold it (`class NS { static Foo = .. }` + `namespace NS { interface Foo }`).
pub(crate) fn is_type_only_token(&self, path: &[Ident<'_>], class: &Class<'_>) -> bool {
let Some(root) = path.first() else { return false };
class.id.as_ref().is_none_or(|id| id.name.as_str() != root.as_str())
&& !self.global_names.contains(root.as_str())
&& !self.merges_a_value(path)
&& self.is_type_only(path)
Comment thread
Brooooooklyn marked this conversation as resolved.
Resolved
}

/// Whether a namespace on `path` (all names but the last) is also a value.
fn merges_a_value(&self, path: &[Ident<'_>]) -> bool {
match path {
[name, rest @ ..] if !rest.is_empty() => {
self.value_decls.contains(name.as_str())
|| self.namespaces.get(name.as_str()).is_some_and(|ns| ns.merges_a_value(rest))
}
_ => false,
}
}
}

/// Extract Angular's decorators from a constructor parameter. With the file
Expand Down
2 changes: 1 addition & 1 deletion crates/oxc_angular_compiler/src/class_metadata/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ mod builders;
mod compiler;
mod metadata;

pub(crate) use builders::{LocalDecls, entity_path, param_type_reference, type_param_names_of};
pub use builders::{
build_ctor_params_metadata, build_ctor_params_metadata_in, build_decorator_metadata_array,
build_prop_decorators_metadata, build_prop_decorators_metadata_in,
};
pub(crate) use builders::{entity_path, param_type_reference, type_param_names_of};
pub use compiler::{
compile_class_metadata, compile_component_class_metadata,
compile_component_metadata_async_resolver, compile_deferred_dependency_import,
Expand Down
14 changes: 12 additions & 2 deletions crates/oxc_angular_compiler/src/component/decorator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -812,9 +812,8 @@ fn extract_constructor_deps<'a>(
// Constructor found - extract parameters (may be empty)
let mut deps = Vec::new_in(&allocator);
let params = &ctor.value.params;

for param in &params.items {
let dep = extract_param_dependency(allocator, param, import_map, consts);
let dep = extract_param_dependency(allocator, class, param, import_map, consts);
deps.push(dep);
}

Expand All @@ -838,6 +837,7 @@ fn extract_constructor_deps<'a>(
/// (see [`crate::directive::angular_param_decorator`]).
fn extract_param_dependency<'a>(
allocator: &'a Allocator,
class: &Class<'a>,
param: &'a oxc_ast::ast::FormalParameter<'a>,
import_map: &ImportMap<'a>,
consts: &StringConsts<'a>,
Expand Down Expand Up @@ -937,6 +937,16 @@ fn extract_param_dependency<'a>(
d.has_named_import = from_inject || import_info.is_default_import;
d
}
} else if !from_inject
&& consts.local_decls().is_some_and(|decls| {
let path: std::vec::Vec<Ident<'a>> =
std::iter::once(*token_name).chain(member_path.iter().copied()).collect();
decls.is_type_only_token(&path, class)
})
{
// An interface or type alias declared in the file is erased like
// a type-only import. See issue #653.
R3DependencyMetadata::type_only_invalid()
} else {
let mut d = R3DependencyMetadata::new(*token_name);
d.token_member_path = member_path;
Expand Down
31 changes: 30 additions & 1 deletion crates/oxc_angular_compiler/src/component/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,9 @@ fn module_export_name_to_str<'a>(name: &ModuleExportName<'a>) -> Option<&'a str>
/// written, and a default import (`import Foo from './foo'`) stays `Foo`
/// (`typeToValue`'s LOCAL reference with a `defaultImportStatement`). Import elision
/// keeps both imports.
///
/// A token that is not imported but names an interface or type alias declared in the
/// file has no value either, so the dep is invalid, as for a type-only import (#653).
fn resolve_factory_dep_namespaces<'a>(
allocator: &'a Allocator,
deps: &mut oxc_allocator::Vec<'a, crate::factory::R3DependencyMetadata<'a>>,
Expand Down Expand Up @@ -765,7 +768,20 @@ fn resolve_factory_dep_namespaces<'a>(
let OutputExpression::ReadVar(var) = root else { continue };
let name = var.name;
// Look up this identifier in the import map
let Some(import_info) = import_map.get(&name) else { continue };
let Some(import_info) = import_map.get(&name) else {
// An interface or type alias declared in the file is erased too, so
// ngtsc has no value for it either (`interface Foo` → `ɵɵinvalidFactory()`).
// See issue #653.
if !from_inject
&& let Some(decls) = string_consts.local_decls()
&& let Some(path) = dep.token.as_ref().and_then(member_chain_path)
&& decls.is_type_only_token(&path, class)
{
dep.token = None;
dep.type_only_invalid = true;
}
continue;
};
if import_info.is_type_only {
// Type-only imports (`import type { X }` / `import { type X }`) are erased
// at runtime, so they cannot be used as DI tokens. Flag the dep so the
Expand Down Expand Up @@ -805,6 +821,19 @@ fn resolve_factory_dep_namespaces<'a>(
}
}

/// The names of a member chain, from its root: `ns.one.One` → `[ns, one, One]`.
fn member_chain_path<'a>(expr: &OutputExpression<'a>) -> Option<std::vec::Vec<Ident<'a>>> {
match expr {
OutputExpression::ReadVar(var) => Some(vec![var.name]),
OutputExpression::ReadProp(prop) => {
let mut path = member_chain_path(&prop.receiver)?;
path.push(prop.name);
Some(path)
}
_ => None,
}
}

/// The leftmost expression of a property-read chain: `ns` in `ns.one.One`.
fn member_chain_root<'b, 'a>(expr: &'b mut OutputExpression<'a>) -> &'b mut OutputExpression<'a> {
match expr {
Expand Down
15 changes: 15 additions & 0 deletions crates/oxc_angular_compiler/src/directive/decorator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,9 @@ pub struct StringConsts<'a> {
program: Option<&'a Program<'a>>,
/// Built on first use: most files have no decorator metadata to evaluate.
scope: std::cell::OnceCell<FileScope<'a>>,
/// The file's type and value declarations, built on first use and shared
/// by every class of the file (see [`Self::local_decls`]).
local_decls: std::cell::OnceCell<crate::class_metadata::LocalDecls<'a>>,
/// Reads the value an imported binding has in its exporting file, like
/// ngtsc's program-wide checker (`None`: imports stay opaque references).
/// `Rc`-shared so it can borrow the analyzer that produced it.
Expand Down Expand Up @@ -521,6 +524,7 @@ impl<'a> StringConsts<'a> {
strings: HashMap::default(),
program: Some(program),
scope: std::cell::OnceCell::new(),
local_decls: std::cell::OnceCell::new(),
#[cfg(feature = "cross_file_elision")]
resolver: None,
host_values: std::cell::RefCell::new(HashMap::default()),
Expand All @@ -532,6 +536,16 @@ impl<'a> StringConsts<'a> {
self.scope.get_or_init(|| self.program.map(FileScope::collect).unwrap_or_default())
}

/// The interfaces, type aliases and values the file declares, to tell a
/// type-only name from a value (`None` without the program).
pub(crate) fn local_decls(&self) -> Option<&crate::class_metadata::LocalDecls<'a>> {
let program = self.program?;
Some(
self.local_decls
.get_or_init(|| crate::class_metadata::LocalDecls::collect(&program.body)),
)
}

/// The program these consts were collected from, when available.
pub(crate) fn program(&self) -> Option<&'a Program<'a>> {
self.program
Expand Down Expand Up @@ -580,6 +594,7 @@ pub fn collect_string_consts<'a>(
strings: HashMap::default(),
program: Some(program),
scope: std::cell::OnceCell::new(),
local_decls: std::cell::OnceCell::new(),
#[cfg(feature = "cross_file_elision")]
resolver: None,
host_values: std::cell::RefCell::new(HashMap::default()),
Expand Down
Loading
Loading