diff --git a/crates/oak_semantic/src/effects.rs b/crates/oak_semantic/src/effects.rs index b9a2c254e..cbacb35c7 100644 --- a/crates/oak_semantic/src/effects.rs +++ b/crates/oak_semantic/src/effects.rs @@ -92,8 +92,8 @@ pub trait EffectHandler: std::fmt::Debug + Sync { /// Resolve this effect for `call`, or `None` when the call isn't in a shape /// this handler recognizes. /// - /// `ctx` resolves information the call's own syntax doesn't carry, e.g. what - /// a `character.only = TRUE` variable is bound to. Unused until that lands. + /// `ctx` provides semantic resolution, e.g. resolve an argument to a + /// statically known string or boolean. fn resolve(&self, call: &RCall, ctx: &CallContext) -> Option; } @@ -304,39 +304,6 @@ impl EffectHandler for ArgumentsAnnotation { } } -/// Declares how an attach function (`library()`, `require()`) names its package, -/// and serves as the default [`EffectHandler`] for it by extracting that package -/// from a call. -#[derive(Debug, Clone, Copy)] -pub struct AttachAnnotation { - /// Whether the callee has a `character.only`-style flag. Unread today. - pub character_only: bool, -} - -impl EffectHandler for AttachAnnotation { - type Output = String; - - fn resolve(&self, call: &RCall, ctx: &CallContext) -> Option { - // `library()`/`require()` name their package in the `package` formal, - // the first positional argument. - let formals = [Formal { - name: "package", - position: 0, - }]; - let matched = ctx.match_arguments(call, &formals); - - let arg_index = matched.iter().position(|formal| *formal == Some(0))?; - let arg = call.arguments().ok()?.items().iter().nth(arg_index)?.ok()?; - let value = arg.value()?; - - match &value { - AnyRExpression::RIdentifier(ident) => Some(ident.name_text()), - AnyRExpression::AnyRValue(AnyRValue::RStringValue(s)) => s.string_text(), - _ => None, - } - } -} - /// Declares how a source function (`source()`) names the file it reads, and /// serves as the default [`EffectHandler`] for it by pulling that path out of a /// call. diff --git a/crates/oak_semantic/src/effects/contrib.rs b/crates/oak_semantic/src/effects/contrib.rs index 89ab2a77d..42c689c76 100644 --- a/crates/oak_semantic/src/effects/contrib.rs +++ b/crates/oak_semantic/src/effects/contrib.rs @@ -67,25 +67,6 @@ macro_rules! quoted { } pub(crate) use quoted; -/// An attach entry: `(package-argument position, has-`character.only`-flag)`. -macro_rules! attach { - ($pkg:literal, $func:literal, $pos:literal, $character_only:literal) => { - $crate::effects::contrib::Entry { - package: $pkg, - function: $func, - effects: $crate::effects::EffectsHandlers { - arguments: None, - attach: Some(&$crate::effects::AttachAnnotation { - character_only: $character_only, - }), - source: None, - assign: None, - }, - } - }; -} -pub(crate) use attach; - /// A source entry: `(path-argument position)`. The function reads and evaluates /// another file, injecting its top-level names into the caller. macro_rules! source { diff --git a/crates/oak_semantic/src/effects/contrib/base.rs b/crates/oak_semantic/src/effects/contrib/base.rs index 8f7025c8c..c28956084 100644 --- a/crates/oak_semantic/src/effects/contrib/base.rs +++ b/crates/oak_semantic/src/effects/contrib/base.rs @@ -6,7 +6,6 @@ use biome_rowan::WalkEvent; use oak_core::syntax_ext::RIdentifierExt; use crate::effects::contrib::assign; -use crate::effects::contrib::attach; use crate::effects::contrib::nse; use crate::effects::contrib::quoted; use crate::effects::contrib::source; @@ -43,9 +42,9 @@ pub(crate) static ENTRIES: &[Entry] = &[ assign: None, }, }, - // base attach - attach!("base", "library", 0, true), - attach!("base", "require", 0, true), + // base attach. `library`/`require` share `LibraryHandler` (below). + attach_entry("library"), + attach_entry("require"), // base source source!("base", "source", 0), // base assign @@ -152,3 +151,73 @@ fn unquote_hole(call: &RCall, splice: bool) -> Option { } call.arguments().ok()?.items().iter().next()?.ok()?.value() } + +/// Build the attach [`Entry`] for a base function served by [`LibraryHandler`]. +const fn attach_entry(function: &'static str) -> Entry { + Entry { + package: "base", + function, + effects: EffectsHandlers { + arguments: None, + attach: Some(&LibraryHandler), + source: None, + assign: None, + }, + } +} + +/// Handler for `library()` and `require()`. Names the attached package from the +/// first argument, read as quoted (the symbol or string as written, so +/// `library(dplyr)` attaches `dplyr`). `character.only = TRUE` flips that +/// argument to standard eval (a value to resolve, `library(pkg, character.only = +/// TRUE)`), matching R. That flag is specific to these callees, so it lives in +/// this handler rather than the shared attach vocabulary. +#[derive(Debug, Clone, Copy)] +pub(crate) struct LibraryHandler; + +impl EffectHandler for LibraryHandler { + type Output = String; + + fn resolve(&self, call: &RCall, ctx: &CallContext) -> Option { + // `character.only` sits at signature position 4 in both callees; in + // practice it's passed by name. + let formals = [ + Formal { + name: "package", + position: 0, + }, + Formal { + name: "character.only", + position: 4, + }, + ]; + let matched = ctx.match_arguments(call, &formals); + + let args = call.arguments().ok()?; + let values: Vec> = args + .items() + .iter() + .map(|item| item.ok().and_then(|arg| arg.value())) + .collect(); + + let package = matched + .iter() + .position(|formal| *formal == Some(0)) + .and_then(|i| values.get(i)) + .and_then(|value| value.as_ref())?; + + let character_only = matched + .iter() + .position(|formal| *formal == Some(1)) + .and_then(|i| values.get(i)) + .and_then(|value| value.as_ref()) + .and_then(|value| ctx.resolve_static_bool(value)) + .unwrap_or(false); + + if character_only { + ctx.resolve_static_string(package) + } else { + ctx.resolve_quoted_symbol_or_string(package) + } + } +} diff --git a/crates/oak_semantic/tests/integration/builder.rs b/crates/oak_semantic/tests/integration/builder.rs index 7233403a8..39ce387b8 100644 --- a/crates/oak_semantic/tests/integration/builder.rs +++ b/crates/oak_semantic/tests/integration/builder.rs @@ -1538,6 +1538,35 @@ fn test_directive_multiple_arguments() { }]); } +#[test] +fn test_directive_character_only_string() { + // `character.only = TRUE` reads the package argument with the standard rule. + // A string literal resolves to its text. + let index = index_with_base("library(\"dplyr\", character.only = TRUE)"); + assert_eq!(semantic_call_kinds(&index), [&SemanticCallKind::Attach { + package: "dplyr".into() + }]); +} + +#[test] +fn test_directive_character_only_identifier_not_attached() { + // With `character.only = TRUE` the package argument is a variable to resolve, + // not a symbol. We can't chase it statically, so nothing is attached, rather + // than wrongly attaching a package literally named `x`. + let index = index_with_base("library(x, character.only = TRUE)"); + assert_eq!(semantic_call_kinds(&index), Vec::<&SemanticCallKind>::new()); +} + +#[test] +fn test_directive_character_only_false_is_quoted() { + // `character.only = FALSE` leaves the package argument quoted, so the symbol + // text is the package name. + let index = index_with_base("library(dplyr, character.only = FALSE)"); + assert_eq!(semantic_call_kinds(&index), [&SemanticCallKind::Attach { + package: "dplyr".into() + }]); +} + #[test] fn test_directive_no_arguments_ignored() { let index = index_with_base("library()");