-
Notifications
You must be signed in to change notification settings - Fork 2
fix(eval): make @Deprecated lazy — only warn on access #58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
7f129ec
84da4ed
a23d050
2ee1fc3
bf34324
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,9 +8,7 @@ use std::path::{Path, PathBuf}; | |||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| use crate::error::{Error, Result}; | ||||||||||||||||||||||||||||||||
| use crate::lexer; | ||||||||||||||||||||||||||||||||
| use crate::parser::{ | ||||||||||||||||||||||||||||||||
| self, Annotation, BinOp, Entry, Expr, Modifier, Module, Property, StringInterpPart, UnOp, | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
| use crate::parser::{self, BinOp, Entry, Expr, Modifier, Module, Property, StringInterpPart, UnOp}; | ||||||||||||||||||||||||||||||||
| use crate::value::{ObjectSource, Value}; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// Evaluates pkl source files to [`Value`]. | ||||||||||||||||||||||||||||||||
|
|
@@ -674,7 +672,6 @@ impl Evaluator { | |||||||||||||||||||||||||||||||
| if has_modifier(mods, Modifier::Local) { | ||||||||||||||||||||||||||||||||
| continue; // already collected | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| check_deprecated(&prop.annotations, &prop.name); | ||||||||||||||||||||||||||||||||
| // Extract renderer converters from the `output` block AST, | ||||||||||||||||||||||||||||||||
| // then skip it (it's not included in the output). | ||||||||||||||||||||||||||||||||
| // Clear any base-inherited converters so child overrides take precedence. | ||||||||||||||||||||||||||||||||
|
|
@@ -748,7 +745,23 @@ impl Evaluator { | |||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| out.retain(|_, v| !matches!(v, Value::Lambda(..))); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| Ok(Value::Object(Arc::new(out), None)) | ||||||||||||||||||||||||||||||||
| // If the module declares any `@Deprecated` properties, attach a | ||||||||||||||||||||||||||||||||
| // minimal ObjectSource carrying just the deprecation map so field | ||||||||||||||||||||||||||||||||
| // access can warn lazily. Modules without @Deprecated keep `None` | ||||||||||||||||||||||||||||||||
| // source to avoid changing amend behavior in the common case. | ||||||||||||||||||||||||||||||||
| let deprecated = collect_deprecated(&module.body); | ||||||||||||||||||||||||||||||||
| let source = if deprecated.is_empty() { | ||||||||||||||||||||||||||||||||
| None | ||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||
| Some(Arc::new(ObjectSource { | ||||||||||||||||||||||||||||||||
| entries: Vec::new(), | ||||||||||||||||||||||||||||||||
| scope: IndexMap::new(), | ||||||||||||||||||||||||||||||||
| is_open: true, | ||||||||||||||||||||||||||||||||
| type_name: None, | ||||||||||||||||||||||||||||||||
| deprecated, | ||||||||||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
| Ok(Value::Object(Arc::new(out), source)) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| #[async_recursion(?Send)] | ||||||||||||||||||||||||||||||||
|
|
@@ -863,7 +876,6 @@ impl Evaluator { | |||||||||||||||||||||||||||||||
| if has_modifier(mods, Modifier::Local) { | ||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| check_deprecated(&prop.annotations, &prop.name); | ||||||||||||||||||||||||||||||||
| // Skip the `default` property — it's a template, not an output entry | ||||||||||||||||||||||||||||||||
| if prop.name == "default" && default_template.is_some() { | ||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||
|
|
@@ -918,6 +930,7 @@ impl Evaluator { | |||||||||||||||||||||||||||||||
| scope: IndexMap::new(), | ||||||||||||||||||||||||||||||||
| is_open: true, | ||||||||||||||||||||||||||||||||
| type_name: Some(tn.clone()), | ||||||||||||||||||||||||||||||||
| deprecated: IndexMap::new(), | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
| *result_src = Some(std::sync::Arc::new(new_src)); | ||||||||||||||||||||||||||||||||
|
|
@@ -985,6 +998,7 @@ impl Evaluator { | |||||||||||||||||||||||||||||||
| scope: child_scope.flatten(), | ||||||||||||||||||||||||||||||||
| is_open: true, // default: allow new properties | ||||||||||||||||||||||||||||||||
| type_name: None, | ||||||||||||||||||||||||||||||||
| deprecated: collect_deprecated(entries), | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
| Ok(Value::Object(Arc::new(map), Some(Arc::new(source)))) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
@@ -1379,11 +1393,13 @@ impl Evaluator { | |||||||||||||||||||||||||||||||
| let mut source_scope = scope.flatten(); | ||||||||||||||||||||||||||||||||
| source_scope.shift_remove("outer"); | ||||||||||||||||||||||||||||||||
| source_scope.shift_remove("this"); | ||||||||||||||||||||||||||||||||
| let deprecated = collect_deprecated(&src_entries); | ||||||||||||||||||||||||||||||||
| let source = ObjectSource { | ||||||||||||||||||||||||||||||||
| entries: src_entries, | ||||||||||||||||||||||||||||||||
| scope: source_scope, | ||||||||||||||||||||||||||||||||
| is_open: true, | ||||||||||||||||||||||||||||||||
| type_name: None, | ||||||||||||||||||||||||||||||||
| deprecated, | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
| Ok(Value::Object(Arc::new(map), Some(Arc::new(source)))) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
@@ -1469,6 +1485,7 @@ impl Evaluator { | |||||||||||||||||||||||||||||||
| scope: IndexMap::new(), | ||||||||||||||||||||||||||||||||
| is_open, | ||||||||||||||||||||||||||||||||
| type_name: tn, | ||||||||||||||||||||||||||||||||
| deprecated: IndexMap::new(), | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
| *src_slot = Some(Arc::new(new_src)); | ||||||||||||||||||||||||||||||||
|
|
@@ -1488,6 +1505,7 @@ impl Evaluator { | |||||||||||||||||||||||||||||||
| scope: IndexMap::new(), | ||||||||||||||||||||||||||||||||
| is_open: true, | ||||||||||||||||||||||||||||||||
| type_name: type_name.clone(), | ||||||||||||||||||||||||||||||||
| deprecated: IndexMap::new(), | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
| Ok(Value::Object(Arc::new(merged), Some(Arc::new(src)))) | ||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||
|
|
@@ -1540,10 +1558,14 @@ impl Evaluator { | |||||||||||||||||||||||||||||||
| _ => {} | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| match &obj { | ||||||||||||||||||||||||||||||||
| Value::Object(map, _) => map | ||||||||||||||||||||||||||||||||
| .get(field) | ||||||||||||||||||||||||||||||||
| .cloned() | ||||||||||||||||||||||||||||||||
| .ok_or_else(|| Error::Eval(format!("field not found: {field}"))), | ||||||||||||||||||||||||||||||||
| Value::Object(map, source) => { | ||||||||||||||||||||||||||||||||
| let val = map | ||||||||||||||||||||||||||||||||
| .get(field) | ||||||||||||||||||||||||||||||||
| .cloned() | ||||||||||||||||||||||||||||||||
| .ok_or_else(|| Error::Eval(format!("field not found: {field}")))?; | ||||||||||||||||||||||||||||||||
| warn_if_deprecated_access(source, field); | ||||||||||||||||||||||||||||||||
| Ok(val) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| _ => Err(Error::Eval(format!( | ||||||||||||||||||||||||||||||||
| "cannot access field '{field}' on {}", | ||||||||||||||||||||||||||||||||
| value_type_name(&obj) | ||||||||||||||||||||||||||||||||
|
|
@@ -1554,7 +1576,13 @@ impl Evaluator { | |||||||||||||||||||||||||||||||
| let obj = self.eval_expr(obj_expr, scope, depth + 1).await?; | ||||||||||||||||||||||||||||||||
| match &obj { | ||||||||||||||||||||||||||||||||
| Value::Null => Ok(Value::Null), | ||||||||||||||||||||||||||||||||
| Value::Object(map, _) => Ok(map.get(field).cloned().unwrap_or(Value::Null)), | ||||||||||||||||||||||||||||||||
| Value::Object(map, source) => { | ||||||||||||||||||||||||||||||||
| let val = map.get(field).cloned().unwrap_or(Value::Null); | ||||||||||||||||||||||||||||||||
| if !matches!(val, Value::Null) { | ||||||||||||||||||||||||||||||||
| warn_if_deprecated_access(source, field); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| Ok(val) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| _ => Err(Error::Eval(format!( | ||||||||||||||||||||||||||||||||
|
Comment on lines
+1615
to
1622
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Fix by checking presence separately from value:
Suggested change
|
||||||||||||||||||||||||||||||||
| "cannot access field '{field}' on {}", | ||||||||||||||||||||||||||||||||
| value_type_name(&obj) | ||||||||||||||||||||||||||||||||
|
|
@@ -2204,6 +2232,7 @@ impl Evaluator { | |||||||||||||||||||||||||||||||
| scope: IndexMap::new(), | ||||||||||||||||||||||||||||||||
| is_open: true, | ||||||||||||||||||||||||||||||||
| type_name: Some(tn.clone()), | ||||||||||||||||||||||||||||||||
| deprecated: IndexMap::new(), | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
| *result_src = Some(std::sync::Arc::new(new_src)); | ||||||||||||||||||||||||||||||||
|
|
@@ -2472,26 +2501,48 @@ fn resolve_package_uri(uri: &str) -> Result<PackageSource> { | |||||||||||||||||||||||||||||||
| Err(Error::Eval(format!("unsupported package URI: {uri}"))) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| fn check_deprecated(annotations: &[Annotation], prop_name: &str) { | ||||||||||||||||||||||||||||||||
| for ann in annotations { | ||||||||||||||||||||||||||||||||
| if ann.name == "Deprecated" { | ||||||||||||||||||||||||||||||||
| // Look for a "message" property in the annotation body | ||||||||||||||||||||||||||||||||
| let mut message = None; | ||||||||||||||||||||||||||||||||
| for entry in &ann.body { | ||||||||||||||||||||||||||||||||
| if let Entry::Property(p) = entry | ||||||||||||||||||||||||||||||||
| && p.name == "message" | ||||||||||||||||||||||||||||||||
| && let Some(Expr::String(s)) = &p.value | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| message = Some(s.clone()); | ||||||||||||||||||||||||||||||||
| /// Collect `@Deprecated` annotations from a list of entries into a map of | ||||||||||||||||||||||||||||||||
| /// property name → optional message. Used to populate `ObjectSource.deprecated` | ||||||||||||||||||||||||||||||||
| /// so field access can warn lazily, instead of warning eagerly when a module | ||||||||||||||||||||||||||||||||
| /// or object body is evaluated. | ||||||||||||||||||||||||||||||||
| fn collect_deprecated(entries: &[Entry]) -> IndexMap<String, Option<String>> { | ||||||||||||||||||||||||||||||||
| let mut out: IndexMap<String, Option<String>> = IndexMap::new(); | ||||||||||||||||||||||||||||||||
| for entry in entries { | ||||||||||||||||||||||||||||||||
| if let Entry::Property(prop) = entry { | ||||||||||||||||||||||||||||||||
| for ann in &prop.annotations { | ||||||||||||||||||||||||||||||||
| eprintln!("[pklr-debug] ann {:?}", ann.name); | ||||||||||||||||||||||||||||||||
|
greptile-apps[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||
| if ann.name != "Deprecated" { | ||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| if let Some(msg) = message { | ||||||||||||||||||||||||||||||||
| eprintln!("[pklr] WARNING: property '{prop_name}' is deprecated: {msg}"); | ||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||
| eprintln!("[pklr] WARNING: property '{prop_name}' is deprecated"); | ||||||||||||||||||||||||||||||||
| let mut message = None; | ||||||||||||||||||||||||||||||||
| for e in &ann.body { | ||||||||||||||||||||||||||||||||
| if let Entry::Property(p) = e | ||||||||||||||||||||||||||||||||
| && p.name == "message" | ||||||||||||||||||||||||||||||||
| && let Some(Expr::String(s)) = &p.value | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| message = Some(s.clone()); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| out.insert(prop.name.clone(), message); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| out | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// If `field` is marked `@Deprecated` in `source`, emit the warning. | ||||||||||||||||||||||||||||||||
| /// Called from field-access expressions so the warning fires when a | ||||||||||||||||||||||||||||||||
| /// deprecated property is *used*, not when its containing module loads. | ||||||||||||||||||||||||||||||||
| fn warn_if_deprecated_access(source: &Option<Arc<ObjectSource>>, field: &str) { | ||||||||||||||||||||||||||||||||
| let Some(src) = source else { return }; | ||||||||||||||||||||||||||||||||
| let Some(message) = src.deprecated.get(field) else { | ||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
| if let Some(msg) = message { | ||||||||||||||||||||||||||||||||
| eprintln!("[pklr] WARNING: property '{field}' is deprecated: {msg}"); | ||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||
| eprintln!("[pklr] WARNING: property '{field}' is deprecated"); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
greptile-apps[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// Resolve a potentially dotted name (e.g. "Foo.Bar") in scope. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.