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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
run: cargo check --all-features

- name: Test
run: cargo test
run: cargo test --all-features

- name: Format
if: ${{ matrix.os == 'ubuntu-latest' }}
Expand Down
937 changes: 789 additions & 148 deletions crates/oxc_angular_compiler/src/component/cross_file_elision.rs

Large diffs are not rendered by default.

113 changes: 83 additions & 30 deletions crates/oxc_angular_compiler/src/component/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,22 @@ pub struct TransformOptions {
#[cfg(feature = "cross_file_elision")]
pub cross_file_elision: bool,

/// Evaluate decorator metadata values imported from other files
/// (`@Directive({inputs: INPUTS})`, `@Input(OPTS)`, ...), the way ngtsc's
/// program-wide checker does.
///
/// When true, imported bindings in evaluated positions resolve to their
/// exporting file's value — following re-exports, `export *` chains and
/// `const` chains across files. Values that can't be resolved or read
/// statically (packages, `.d.ts` declarations, non-literals) stay opaque
/// and keep the "cannot evaluate values from other files" diagnostic.
///
/// Files read this way are reported in `TransformResult::dependencies`.
/// Uses `base_dir`/`tsconfig_path` for module resolution, like
/// `cross_file_elision`.
#[cfg(feature = "cross_file_elision")]
pub resolve_imported_values: bool,

/// Base directory for module resolution.
///
/// Used when `cross_file_elision` is enabled to resolve relative imports.
Expand Down Expand Up @@ -250,6 +266,8 @@ impl Default for TransformOptions {
#[cfg(feature = "cross_file_elision")]
cross_file_elision: false,
#[cfg(feature = "cross_file_elision")]
resolve_imported_values: false,
#[cfg(feature = "cross_file_elision")]
base_dir: None,
#[cfg(feature = "cross_file_elision")]
tsconfig_path: None,
Expand Down Expand Up @@ -2922,47 +2940,62 @@ pub fn transform_angular_file(
// by tracking the next index and passing it to each component.
let mut shared_pool_index: u32 = 0;

// One analyzer serves both cross-file features: the type-only scan for
// import elision and, when resolve_imported_values is on, the resolver the
// decorator evaluator reads imported bindings through. It's declared
// before `string_consts` so the resolver borrowed by it drops first.
#[cfg(feature = "cross_file_elision")]
let cross_file_analyzer: Option<CrossFileAnalyzer> =
if options.cross_file_elision || options.resolve_imported_values {
let file_path = std::path::Path::new(path);
let base_dir = options.base_dir.as_deref().or_else(|| file_path.parent());
base_dir.map(|base| {
CrossFileAnalyzer::new(
base,
options.tsconfig_path.as_deref(),
options.resolve_imported_values,
)
})
} else {
None
};

// When cross_file_elision is enabled, collect type-only information for each import
// by checking if the exported symbol is an interface or type alias. This is separate
// from barrel resolution to avoid changing namespace import paths.
#[cfg(feature = "cross_file_elision")]
let cross_file_type_only: FxHashMap<String, bool> = if options.cross_file_elision {
let cross_file_type_only: FxHashMap<String, bool> = if options.cross_file_elision
&& let Some(analyzer) = &cross_file_analyzer
{
let file_path = std::path::Path::new(path);
let base_dir = options.base_dir.as_deref().or_else(|| file_path.parent());

if let Some(base) = base_dir {
let mut analyzer = CrossFileAnalyzer::new(base, options.tsconfig_path.as_deref());
let mut type_only: FxHashMap<String, bool> = FxHashMap::default();
let mut type_only: FxHashMap<String, bool> = FxHashMap::default();

for stmt in &parser_ret.program.body {
let Statement::ImportDeclaration(import_decl) = stmt else {
continue;
};
for stmt in &parser_ret.program.body {
let Statement::ImportDeclaration(import_decl) = stmt else {
continue;
};

let source = import_decl.source.value.as_str();
let Some(specifiers) = &import_decl.specifiers else {
continue;
};
let source = import_decl.source.value.as_str();
let Some(specifiers) = &import_decl.specifiers else {
continue;
};

for specifier in specifiers {
if let ImportDeclarationSpecifier::ImportSpecifier(spec) = specifier {
let local_name = spec.local.name.as_str();
let imported_name = spec.imported.name().as_str();
for specifier in specifiers {
if let ImportDeclarationSpecifier::ImportSpecifier(spec) = specifier {
let local_name = spec.local.name.as_str();
let imported_name = spec.imported.name().as_str();

// Check if this import is type-only using the original import path.
// Resolves the file and checks if the exported symbol is an interface
// or type alias. Unresolvable imports return false (conservative).
if analyzer.is_type_only_import(source, imported_name, file_path) {
type_only.insert(local_name.to_string(), true);
}
// Check if this import is type-only using the original import path.
// Resolves the file and checks if the exported symbol is an interface
// or type alias. Unresolvable imports return false (conservative).
if analyzer.is_type_only_import(source, imported_name, file_path) {
type_only.insert(local_name.to_string(), true);
}
}
}

type_only
} else {
FxHashMap::default()
}

type_only
} else {
FxHashMap::default()
};
Expand All @@ -2977,8 +3010,18 @@ pub fn transform_angular_file(

// Collect file-scope string consts so decorator metadata can resolve identifier
// references (e.g. `host: { [ATTR_NAME]: '' }`) the same way the official
// Angular compiler does.
let string_consts = collect_string_consts(allocator, &parser_ret.program);
// Angular compiler does. `resolve_imported_values` teaches the evaluator
// to read imported bindings the way ngtsc's checker does.
#[allow(unused_mut)]
let mut string_consts = collect_string_consts(allocator, &parser_ret.program);
#[cfg(feature = "cross_file_elision")]
if options.resolve_imported_values
&& let Some(analyzer) = &cross_file_analyzer
{
string_consts = string_consts.with_resolver(std::rc::Rc::new(
analyzer.value_resolver(std::path::PathBuf::from(path)),
));
}

#[cfg(feature = "cross_file_elision")]
let mut import_map =
Expand Down Expand Up @@ -4315,6 +4358,16 @@ pub fn transform_angular_file(
result.code = apply_edits(source, edits);
}

// Files read to resolve imported metadata values become watch
// dependencies, like template/style URLs — only when the option that
// asked for those reads is on.
#[cfg(feature = "cross_file_elision")]
if options.resolve_imported_values
&& let Some(analyzer) = &cross_file_analyzer
{
result.dependencies.extend(analyzer.take_value_dependencies());
}
Comment thread
Brooooooklyn marked this conversation as resolved.

result
}

Expand Down
25 changes: 25 additions & 0 deletions crates/oxc_angular_compiler/src/directive/decorator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,11 @@ 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>>,
/// 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.
#[cfg(feature = "cross_file_elision")]
resolver: Option<std::rc::Rc<dyn super::evaluator::ImportValueResolver>>,
}

impl<'a> StringConsts<'a> {
Expand All @@ -505,6 +510,22 @@ impl<'a> StringConsts<'a> {
self.strings.get(name)
}

/// The import value resolver attached to these consts, if any.
#[cfg(feature = "cross_file_elision")]
pub(crate) fn resolver(&self) -> Option<&dyn super::evaluator::ImportValueResolver> {
self.resolver.as_deref()
}

/// Attach an import value resolver (see [`Self::resolver`]).
#[cfg(feature = "cross_file_elision")]
pub(crate) fn with_resolver(
mut self,
resolver: std::rc::Rc<dyn super::evaluator::ImportValueResolver>,
) -> Self {
self.resolver = Some(resolver);
self
}

/// `program`'s declarations without its folded strings: enough to tell
/// where a name is imported from (see
/// [`super::angular_param_decorator`]).
Expand All @@ -513,6 +534,8 @@ impl<'a> StringConsts<'a> {
strings: HashMap::default(),
program: Some(program),
scope: std::cell::OnceCell::new(),
#[cfg(feature = "cross_file_elision")]
resolver: None,
}
}

Expand Down Expand Up @@ -564,6 +587,8 @@ pub fn collect_string_consts<'a>(
strings: HashMap::default(),
program: Some(program),
scope: std::cell::OnceCell::new(),
#[cfg(feature = "cross_file_elision")]
resolver: None,
};
loop {
let before = map.strings.len();
Expand Down
Loading
Loading