Skip to main content

bynk_lsp/
provider_stub.rs

1//! #2100 slice 4: generate a provider stub. For a capability declared in this
2//! file that nothing in its context provides, the action scaffolds
3//! `provides Cap = CapImpl { … }` right below the declaration, with every
4//! operation's signature copied and its body a typed default.
5//!
6//! It is offered as a quick fix on `bynk.exports.capability_not_provided`
7//! (an exported capability with no provider), and as a `refactor` with the
8//! cursor on the capability's name. The Workers-only
9//! `bynk.capability.not_provided` is never raised in the editor, whose
10//! analysis runs as the bundle target, so the refactor is what covers a
11//! required capability.
12//!
13//! The language has no placeholder expression, so a stub body must be a value
14//! that type-checks (DECISION B re-checks the edit). Each body is a default of
15//! the operation's return type: the unrefined scalars, `()`, `None`, `[]`, or
16//! one of those inside an `Effect`. A capability with an operation returning
17//! anything else (a record, a user sum, a `Result`) gets no action. So does a
18//! derived capability, one declared outside a context, or one with generic
19//! operations (whose type parameters the stub would have to repeat).
20//!
21//! Whether a capability already has a provider is read from the committed
22//! index's implementation edges, so a provider in a sibling file counts. Like
23//! fill-arms, the action is listed without an edit, and `codeAction/resolve`
24//! computes and re-checks it. A `bynk-fmt`-canonical file is re-formatted and
25//! the edit narrowed to the changed region.
26
27use bynk_check::index::{ProjectIndex, SymbolKey, SymbolKind};
28use bynk_syntax::ast::{BaseType, CapabilityDecl, CommonsItem, SourceUnit, TypeRef};
29use bynk_syntax::lexer::tokenize;
30use bynk_syntax::parser::parse_unit_with_recovery;
31use bynk_syntax::span::Span;
32use tower_lsp::lsp_types::*;
33
34const DATA_TAG: &str = "bynk.generateProvider";
35const CATEGORIES: [&str; 1] = ["bynk.exports.capability_not_provided"];
36
37/// A planned stub: the capability, the provider's name, and the edits.
38pub struct Plan {
39    pub capability: String,
40    pub provider: String,
41    pub edits: Vec<(Span, String)>,
42}
43
44/// One capability the request points at, and how: through a "no provider"
45/// diagnostic (a quick fix) or its declaration's name (a refactor).
46pub struct Request {
47    pub capability: String,
48    pub quick_fix: bool,
49}
50
51/// The capabilities at `requested`, by diagnostic first, each named once.
52pub fn requested(
53    text: &str,
54    diagnostics: &[bynk_ide::Diagnostic],
55    requested: Span,
56) -> Vec<Request> {
57    let mut out: Vec<Request> = Vec::new();
58    let touches = |s: Span| s.start <= requested.end && requested.start <= s.end;
59    for d in diagnostics {
60        if CATEGORIES.contains(&d.error.category) && touches(d.error.span) {
61            let name = text
62                .get(d.error.span.start..d.error.span.end)
63                .unwrap_or_default()
64                .trim()
65                .to_string();
66            if !name.is_empty() && !out.iter().any(|r| r.capability == name) {
67                out.push(Request {
68                    capability: name,
69                    quick_fix: true,
70                });
71            }
72        }
73    }
74    if let Some(unit) = parse(text) {
75        for cap in capabilities(&unit) {
76            if touches(cap.name.span) && !out.iter().any(|r| r.capability == cap.name.name) {
77                out.push(Request {
78                    capability: cap.name.name.clone(),
79                    quick_fix: false,
80                });
81            }
82        }
83    }
84    out
85}
86
87/// The stub for `capability` in `text`, or `None` when there is none to offer
88/// (see the module docs).
89pub fn plan(text: &str, capability: &str, index: &ProjectIndex) -> Option<Plan> {
90    let unit = parse(text)?;
91    let SourceUnit::Context(ctx) = &unit else {
92        return None;
93    };
94    let cap = capabilities(&unit)
95        .into_iter()
96        .find(|c| c.name.name == capability)?;
97    if cap.derived.is_some() || cap.ops.is_empty() {
98        return None;
99    }
100    let key = SymbolKey {
101        unit: ctx.name.joined(),
102        kind: SymbolKind::Capability,
103        name: capability.to_string(),
104    };
105    if index.impls_of(&key).next().is_some() {
106        return None;
107    }
108    // Names the context already declares anywhere (a sibling file's too,
109    // from the committed index) are taken, as well as words in this file.
110    let unit = ctx.name.joined();
111    let declared: Vec<&str> = index
112        .symbols
113        .keys()
114        .filter(|k| k.unit == unit)
115        .map(|k| k.name.as_str())
116        .collect();
117    let provider = fresh_name(text, &declared, &format!("{capability}Impl"));
118
119    let mut ops = Vec::new();
120    for op in &cap.ops {
121        if !op.type_params.is_empty() {
122            return None;
123        }
124        let body = default_of(&op.return_type)?;
125        let signature = text.get(op.span.start..op.return_type.span().end)?;
126        if signature.contains("//") || !signature.trim_start().starts_with("fn") {
127            return None;
128        }
129        ops.push(format!("  {} {{\n    {body}\n  }}", signature.trim()));
130    }
131    let stub = format!(
132        "\n\nprovides {capability} = {provider} {{\n{}\n}}",
133        ops.join("\n\n")
134    );
135    let mut edits = vec![(Span::new(cap.span.end, cap.span.end), stub)];
136
137    let opts = bynk_fmt::FormatOptions::default();
138    if bynk_fmt::format_source(text, &opts).is_ok_and(|f| f == text) {
139        let stubbed = crate::fix_all::apply(text, &edits);
140        let formatted = bynk_fmt::format_source(&stubbed, &opts).ok()?;
141        edits = vec![crate::inline::changed_region(text, &formatted)];
142    }
143    Some(Plan {
144        capability: capability.to_string(),
145        provider,
146        edits,
147    })
148}
149
150fn parse(text: &str) -> Option<SourceUnit> {
151    let tokens = tokenize(text).ok()?;
152    match parse_unit_with_recovery(&tokens, text) {
153        (Some(unit), errs) if errs.is_empty() => Some(unit),
154        _ => None,
155    }
156}
157
158fn capabilities(unit: &SourceUnit) -> Vec<&CapabilityDecl> {
159    let SourceUnit::Context(ctx) = unit else {
160        return Vec::new();
161    };
162    ctx.items
163        .iter()
164        .filter_map(|i| match i {
165            CommonsItem::Capability(c) => Some(c),
166            _ => None,
167        })
168        .collect()
169}
170
171/// A body sure to type-check as `ty` (the #852 `field_default_init` set plus
172/// unit), looking through one `Effect` (an op body auto-lifts).
173fn default_of(ty: &TypeRef) -> Option<&'static str> {
174    let ty = match ty {
175        TypeRef::Effect(inner, _) => inner,
176        other => other,
177    };
178    Some(match ty {
179        TypeRef::Base(BaseType::Int, _) => "0",
180        TypeRef::Base(BaseType::Float, _) => "0.0",
181        TypeRef::Base(BaseType::String, _) => "\"\"",
182        TypeRef::Base(BaseType::Bool, _) => "false",
183        TypeRef::Unit(_) => "()",
184        TypeRef::Option(..) => "None",
185        TypeRef::List(..) => "[]",
186        _ => return None,
187    })
188}
189
190/// `base`, or `base2`, `base3`, … — the first that is neither a word of
191/// `text` nor one of the context's `declared` names.
192fn fresh_name(text: &str, declared: &[&str], base: &str) -> String {
193    let taken = |name: &str| {
194        declared.contains(&name)
195            || text.match_indices(name).any(|(i, _)| {
196                let before = text[..i].chars().next_back();
197                let after = text[i + name.len()..].chars().next();
198                !before.is_some_and(|c| c.is_alphanumeric() || c == '_')
199                    && !after.is_some_and(|c| c.is_alphanumeric() || c == '_')
200            })
201    };
202    if !taken(base) {
203        return base.to_string();
204    }
205    (2..)
206        .map(|n| format!("{base}{n}"))
207        .find(|name| !taken(name))
208        .expect("an unused name")
209}
210
211/// The listed (unresolved) action for `plan`.
212pub fn listed(
213    plan: &Plan,
214    quick_fix: bool,
215    uri: &Url,
216    version: Option<i32>,
217) -> CodeActionOrCommand {
218    CodeActionOrCommand::CodeAction(CodeAction {
219        title: format!(
220            "Generate provider `{}` for `{}`",
221            plan.provider, plan.capability
222        ),
223        kind: Some(if quick_fix {
224            CodeActionKind::QUICKFIX
225        } else {
226            CodeActionKind::REFACTOR
227        }),
228        data: Some(serde_json::json!({
229            "kind": DATA_TAG,
230            "uri": uri.as_str(),
231            "version": version,
232            "capability": plan.capability,
233        })),
234        ..Default::default()
235    })
236}
237
238/// What a listed stub action's `data` names.
239pub struct Target {
240    pub uri: Url,
241    pub version: Option<i32>,
242    pub capability: String,
243}
244
245impl Target {
246    pub fn of(action: &CodeAction) -> Option<Self> {
247        let data = action.data.as_ref()?;
248        if data.get("kind")?.as_str()? != DATA_TAG {
249            return None;
250        }
251        Some(Target {
252            uri: Url::parse(data.get("uri")?.as_str()?).ok()?,
253            version: data
254                .get("version")
255                .and_then(serde_json::Value::as_i64)
256                .and_then(|v| i32::try_from(v).ok()),
257            capability: data.get("capability")?.as_str()?.to_string(),
258        })
259    }
260}