1use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12
13use bynk_syntax::ast::*;
14use bynk_syntax::lexer::tokenize;
15use bynk_syntax::parser::parse_unit_with_recovery;
16use bynk_syntax::span::Span;
17
18pub fn find_declaration_span(source: &str, name: &str) -> Option<Span> {
35 let tokens = tokenize(source).ok()?;
36 let (unit, _errs) = parse_unit_with_recovery(&tokens, source);
37 let unit = unit?;
38 let items: &[CommonsItem] = match &unit {
39 SourceUnit::Commons(c) => &c.items,
40 SourceUnit::Context(c) => &c.items,
41 SourceUnit::Adapter(a) => &a.items,
42 SourceUnit::Suite(_) => &[],
43 };
44 items
45 .iter()
46 .filter_map(CommonsItem::name)
47 .find(|ident| ident.name == name)
48 .map(|ident| ident.span)
49}
50
51pub fn describe_symbol(source: &str, name: &str) -> Option<String> {
54 let tokens = tokenize(source).ok()?;
55 let (unit, _errs) = parse_unit_with_recovery(&tokens, source);
56 let unit = unit?;
57 let items: &[CommonsItem] = match &unit {
58 SourceUnit::Commons(c) => &c.items,
59 SourceUnit::Context(c) => &c.items,
60 SourceUnit::Adapter(a) => &a.items,
61 SourceUnit::Suite(_) => &[],
62 };
63 if let SourceUnit::Context(ctx) = &unit
66 && let Some(c) = items.iter().find_map(|item| match item {
67 CommonsItem::Capability(c) if c.name.name == name && c.derived.is_some() => Some(c),
68 _ => None,
69 })
70 && let Some(d) = &c.derived
71 {
72 let base_unit = ctx.consumes.iter().find_map(|cd| {
73 cd.selected
74 .as_ref()?
75 .iter()
76 .any(|s| s.name == d.base.name)
77 .then(|| cd.target.joined())
78 });
79 let base = base_unit
80 .as_deref()
81 .and_then(|u| crate::completion::embedded_capability(u, &d.base.name));
82 return Some(describe_derived_capability(
83 c,
84 base_unit.as_deref(),
85 base.as_ref(),
86 ));
87 }
88 for item in items {
89 if let Some(summary) = describe_item(item, name) {
90 return Some(summary);
91 }
92 }
93 None
94}
95
96pub fn describe_keyword_at(source: &str, offset: usize) -> Option<&'static str> {
104 let tokens = tokenize(source).ok()?;
105 let word = tokens
106 .iter()
107 .find(|t| t.span.start <= offset && offset < t.span.end)
108 .map(|t| &source[t.span.start..t.span.end])?;
109 bynk_syntax::keywords::KEYWORDS
110 .iter()
111 .find(|k| k.word == word)
112 .map(|k| k.meaning)
113}
114
115pub fn describe_agent_state_at(source: &str, offset: usize) -> Option<String> {
136 let tokens = tokenize(source).ok()?;
137 let (unit, _errs) = parse_unit_with_recovery(&tokens, source);
138 let unit = unit?;
139 let items: &[CommonsItem] = match &unit {
140 SourceUnit::Commons(c) => &c.items,
141 SourceUnit::Context(c) => &c.items,
142 SourceUnit::Adapter(a) => &a.items,
143 SourceUnit::Suite(_) => &[],
144 };
145 if let SourceUnit::Context(c) = &unit {
148 for r in &c.removed_agents {
149 if span_covers(r.keyword, offset) || span_covers(r.name.span, offset) {
150 return Some(format!(
151 "```bynk\nremoved agent {}\n```\n\nThe agent class `{}` is gone: its stored \
152 data is deleted at the next deploy, which needs `--confirm-delete \
153 <context>.{}`.",
154 r.name.name, r.name.name, r.name.name
155 ));
156 }
157 }
158 }
159 for item in items {
160 let CommonsItem::Agent(a) = item else {
161 continue;
162 };
163 if let Some(old) = &a.replaces {
166 let on_kw = preceding_ident_span(&tokens, source, old.span, "replaces")
167 .is_some_and(|s| span_covers(s, offset));
168 if on_kw || span_covers(old.span, offset) {
169 return Some(format!(
170 "```bynk\nagent {} replaces {}\n```\n\nRenames the agent class `{}` to `{}`: \
171 its stored data and schema history move to `{}`.",
172 a.name.name, old.name, old.name, a.name.name, a.name.name
173 ));
174 }
175 }
176 let on_key_kw = preceding_ident_span(&tokens, source, a.key_name.span, "key")
179 .is_some_and(|s| span_covers(s, offset));
180 if on_key_kw || span_covers(a.key_name.span, offset) {
181 return Some(key_hover(a));
182 }
183 for f in &a.store_fields {
187 let store_kw = Span {
188 file: f.span.file,
189 start: f.span.start,
190 end: f.span.start + "store".len(),
191 };
192 let on_store_kw = source.get(store_kw.start..store_kw.end) == Some("store")
193 && span_covers(store_kw, offset);
194 if on_store_kw || span_covers(f.name.span, offset) {
195 return Some(store_field_hover(f));
196 }
197 }
198 for m in &a.migrations {
201 if span_covers(m.keyword, offset)
202 && source.get(m.keyword.start..m.keyword.end) == Some("migrate")
203 {
204 return Some(migrate_hover(m));
205 }
206 for item in &m.items {
207 if let MigrateItem::Drop(d) = item
208 && span_covers(d.keyword, offset)
209 {
210 return Some(format!(
211 "```bynk\ndrop {}\n```\n\nDiscards the stored value of `{}` on purpose: \
212 a removed field the step doesn't read, or a changed `Set`, `Log` or \
213 `Cache` field, which starts afresh.",
214 d.field.name, d.field.name
215 ));
216 }
217 }
218 }
219 if !in_state_scope(a, offset) {
221 continue;
222 }
223 let Some((name, name_span)) = ident_at(&tokens, source, offset) else {
224 continue;
225 };
226 if is_dot_preceded(source, name_span.start) {
229 continue;
230 }
231 if name == a.key_name.name {
232 return Some(key_hover(a));
233 }
234 if let Some(f) = a.store_fields.iter().find(|f| f.name.name == name) {
235 return Some(store_field_hover(f));
236 }
237 }
238 None
239}
240
241pub const RESERVED_PRINCIPALS: &[(&str, &str)] = &[
245 (
246 "Nobody",
247 "Sends no credential — no `Authorization` header on a Bearer route, no signature headers on a `Signature` route — so the real seam refuses it (`Rejected(Unauthorized)`). `system` tier only.",
248 ),
249 (
250 "Forged",
251 "Signs a `Signature` route's request with a secret the seam does not hold, so the real seam refuses it (`Rejected(Unauthorized)`). `system` tier only.",
252 ),
253 (
254 "Stale",
255 "Signs a `Signature` route's request correctly, timestamped `tolerance + 60` seconds in the past, so the real seam refuses it as a replay (`Rejected(Unauthorized)`). Needs an actor with `timestamp` and `tolerance`. `system` tier only.",
256 ),
257];
258
259pub fn describe_reserved_principal_at(source: &str, offset: usize) -> Option<String> {
263 let tokens = tokenize(source).ok()?;
264 let (name, span) = ident_at(&tokens, source, offset)?;
265 let (_, doc) = RESERVED_PRINCIPALS.iter().find(|(n, _)| *n == name)?;
266 preceding_ident_span(&tokens, source, span, "by")
267 .map(|_| format!("```bynk\nby {name}\n```\n\n{doc}"))
268}
269
270pub const ENV_KEYWORD_DOC: &str = "The variables and secrets a suite's participants see (`env { NAME = \"value\" … }`): on each Worker's `env` at `system`, and what the `Secrets` double answers at `unit`/`integration`. A case's `env` layers over its suite's, name by name.";
272
273pub fn describe_env_at(source: &str, offset: usize) -> Option<String> {
280 let tokens = tokenize(source).ok()?;
281 let (unit, _errs) = parse_unit_with_recovery(&tokens, source);
282 let SourceUnit::Suite(suite) = unit? else {
283 return None;
284 };
285 let on_kw = |e: &EnvClause| {
286 span_covers(
287 Span {
288 file: e.span.file,
289 start: e.span.start,
290 end: e.span.start + "env".len(),
291 },
292 offset,
293 )
294 };
295 let suite_gives = |name: &str| {
296 suite
297 .envs
298 .iter()
299 .flat_map(|e| e.entries.iter())
300 .any(|x| x.name.name == name)
301 };
302 let render = |entry: &EnvEntry, scope: &str| {
303 let value = match &entry.value.kind {
304 ExprKind::StrLit(v) => format!("{v:?}"),
305 _ => "…".to_string(),
306 };
307 format!(
308 "```bynk\n{} = {value}\n```\n\nvariable given to every participant ({scope})",
309 entry.name.name
310 )
311 };
312 for e in &suite.envs {
313 if on_kw(e) {
314 return Some(ENV_KEYWORD_DOC.to_string());
315 }
316 if let Some(entry) = e.entries.iter().find(|x| span_covers(x.name.span, offset)) {
317 return Some(render(entry, "suite"));
318 }
319 }
320 for case in &suite.cases {
321 for e in &case.envs {
322 if on_kw(e) {
323 return Some(ENV_KEYWORD_DOC.to_string());
324 }
325 if let Some(entry) = e.entries.iter().find(|x| span_covers(x.name.span, offset)) {
326 let scope = if suite_gives(&entry.name.name) {
327 "case, overrides suite"
328 } else {
329 "case"
330 };
331 return Some(render(entry, scope));
332 }
333 }
334 }
335 None
336}
337
338fn in_state_scope(a: &AgentDecl, offset: usize) -> bool {
345 a.handlers.iter().any(|h| span_covers(h.body.span, offset))
346 || a.invariants
347 .iter()
348 .any(|i| span_covers(i.predicate.span, offset))
349 || a.transitions
350 .iter()
351 .any(|t| span_covers(t.predicate.span, offset))
352}
353
354fn migrate_hover(m: &MigrateDecl) -> String {
356 let old = m
357 .old
358 .as_ref()
359 .map(|o| format!(" ({}: {})", o.name.name, type_ref_str(&o.type_ref)))
360 .unwrap_or_default();
361 format!(
362 "```bynk\nmigrate from {}{old}\n```\n\nA migration step: carries a stored record from \
363 schema version {} to the agent's current shape when it next loads.",
364 m.from, m.from
365 )
366}
367
368fn key_hover(a: &AgentDecl) -> String {
370 let sig = format!("key {}: {}", a.key_name.name, type_ref_str(&a.key_type));
371 render_state_hover(&sig, "key")
372}
373
374fn store_field_hover(f: &StoreField) -> String {
376 let mut sig = format!("store {}: {}", f.name.name, store_kind_str(&f.kind));
377 for ann in &f.annotations {
378 sig.push(' ');
379 sig.push_str(&bynk_fmt::annotation_to_string(ann));
380 }
381 render_state_hover(&sig, "store")
382}
383
384pub fn describe_store_op_at(
397 source: &str,
398 offset: usize,
399 locals: &[bynk_check::locals::LocalBinding],
400) -> Option<String> {
401 let tokens = tokenize(source).ok()?;
402 let (unit, _errs) = parse_unit_with_recovery(&tokens, source);
403 let unit = unit?;
404 let items: &[CommonsItem] = match &unit {
405 SourceUnit::Commons(c) => &c.items,
406 SourceUnit::Context(c) => &c.items,
407 SourceUnit::Adapter(a) => &a.items,
408 SourceUnit::Suite(_) => &[],
409 };
410 let (op, op_span) = ident_at(&tokens, source, offset)?;
412 let (recv, recv_start) = receiver_segment_at(source, op_span)?;
413 if is_dot_preceded(source, recv_start) {
417 return None;
418 }
419 if bynk_check::locals::locals_at(locals, recv_start)
422 .iter()
423 .any(|b| b.name == recv)
424 {
425 return None;
426 }
427 for item in items {
428 let CommonsItem::Agent(a) = item else {
429 continue;
430 };
431 if !in_state_scope(a, offset) {
432 continue;
433 }
434 let f = a.store_fields.iter().find(|f| f.name.name == recv)?;
435 let sig = bynk_check::store_ops::ops_for(&f.kind.head.name)
436 .iter()
437 .find(|o| o.name == op)?
438 .signature;
439 return Some(format!(
440 "```bynk\n{sig}\n```\n\nA `{}` store operation on `store {}: {}` — the field's \
441 declared kind grounds the operation's type parameters.",
442 f.kind.head.name,
443 f.name.name,
444 store_kind_str(&f.kind),
445 ));
446 }
447 None
448}
449
450fn ident_at<'a>(
453 tokens: &[bynk_syntax::lexer::Token],
454 source: &'a str,
455 offset: usize,
456) -> Option<(&'a str, Span)> {
457 tokens
458 .iter()
459 .find(|t| t.kind == bynk_syntax::lexer::TokenKind::Ident && span_covers(t.span, offset))
460 .and_then(|t| Some((source.get(t.span.start..t.span.end)?, t.span)))
461}
462
463fn receiver_segment_at(text: &str, member_span: Span) -> Option<(&str, usize)> {
470 let before = text.get(..member_span.start)?.strip_suffix('.')?;
471 crate::completion::ident_ending_at(before, before.len())
472}
473
474pub(crate) fn is_dot_preceded(text: &str, start: usize) -> bool {
477 text[..start].ends_with('.')
478}
479
480pub fn store_field_kind_at(
498 source: &str,
499 recv_end: usize,
500 locals: &[bynk_check::locals::LocalBinding],
501) -> Option<(String, bool)> {
502 let (recv, recv_start) = crate::completion::ident_ending_at(source, recv_end)?;
503 if bynk_check::locals::locals_at(locals, recv_start)
504 .iter()
505 .any(|b| b.name == recv)
506 {
507 return None;
508 }
509 let tokens = tokenize(source).ok()?;
510 let (unit, _errs) = parse_unit_with_recovery(&tokens, source);
511 let unit = unit?;
512 let items: &[CommonsItem] = match &unit {
513 SourceUnit::Commons(c) => &c.items,
514 SourceUnit::Context(c) => &c.items,
515 SourceUnit::Adapter(a) => &a.items,
516 SourceUnit::Suite(_) => &[],
517 };
518 for item in items {
519 let CommonsItem::Agent(a) = item else {
520 continue;
521 };
522 if !in_state_scope(a, recv_end) {
523 continue;
524 }
525 if let Some(f) = a.store_fields.iter().find(|f| f.name.name == recv) {
526 let held = f.kind.head.name == "Map"
527 && f.kind.args.len() == 2
528 && matches!(f.kind.args[1], TypeRef::Connection(..));
529 return Some((f.kind.head.name.clone(), held));
530 }
531 }
532 None
533}
534
535pub fn describe_handler_annotation_at(source: &str, offset: usize) -> Option<String> {
542 let tokens = tokenize(source).ok()?;
543 let (unit, _errs) = parse_unit_with_recovery(&tokens, source);
544 let unit = unit?;
545 let items: &[CommonsItem] = match &unit {
546 SourceUnit::Commons(c) => &c.items,
547 SourceUnit::Context(c) => &c.items,
548 SourceUnit::Adapter(a) => &a.items,
549 SourceUnit::Suite(_) => &[],
550 };
551 for item in items {
552 let (handlers, service_cap): (&[Handler], Option<i64>) = match item {
553 CommonsItem::Service(s) => (&s.handlers, s.limits.as_ref().and_then(|l| l.max_body())),
554 CommonsItem::Agent(a) => (&a.handlers, None),
555 _ => continue,
556 };
557 for h in handlers {
558 for ann in &h.annotations {
559 if span_covers(ann.span, offset) {
560 return Some(render_handler_annotation_hover(ann, h, service_cap));
561 }
562 }
563 }
564 }
565 None
566}
567
568pub fn describe_advance_at(source: &str, offset: usize) -> Option<String> {
572 let tokens = tokenize(source).ok()?;
573 let idx = tokens
574 .iter()
575 .position(|t| t.span.start <= offset && offset < t.span.end)?;
576 let text = |i: usize| tokens.get(i).map(|t| &source[t.span.start..t.span.end]);
577 if text(idx)? != "advance"
578 || tokens.get(idx + 1)?.kind != bynk_syntax::lexer::TokenKind::Ident
579 || tokens.get(idx + 2)?.kind != bynk_syntax::lexer::TokenKind::By
580 {
581 return None;
582 }
583 Some(
584 "```bynk\nadvance Clock by <Duration>\n```\n\nMoves the test harness's clock forward. Each case's clock starts at the epoch; at `unit` and `integration` the `Clock` double reads it, and at `system` every participant's first-party `Clock` does. It governs `Clock` only: JWT expiry, the `Signature` tolerance and Durable Object alarms keep wall-clock time."
585 .to_string(),
586 )
587}
588
589pub const RUN_ID_DOC: &str = "**`run.id`** — this test run's identity, a `String`. In process it is always `\"local\"`; each `bynk test --against` run gets a fresh one, so a key built from it (`\"order-\\(run.id)\"`) doesn't collide with an earlier run's data on a shared deployment.";
591
592pub fn is_test_run_receiver(source: &str, run_start: usize) -> bool {
602 let Ok(tokens) = tokenize(source) else {
603 return false;
604 };
605 let (unit, _errs) = parse_unit_with_recovery(&tokens, source);
606 let Some(SourceUnit::Suite(suite)) = unit else {
607 return false;
608 };
609 let Some(case) = suite.cases.iter().find(|c| span_covers(c.span, run_start)) else {
610 return false;
611 };
612 run_bound_in_block(&case.body, run_start, &mut Vec::new()) != Some(true)
613}
614
615fn run_bound_in_block(block: &Block, at: usize, bound: &mut Vec<String>) -> Option<bool> {
618 let mark = bound.len();
619 let mut found = None;
620 for stmt in &block.statements {
621 let mut exprs = Vec::new();
622 statement_exprs(stmt, &mut exprs);
623 found = exprs
624 .into_iter()
625 .find_map(|e| run_bound_in_expr(e, at, bound));
626 if found.is_some() {
627 break;
628 }
629 if let Statement::Let(l) | Statement::EffectLet(l) = stmt {
631 bound.push(l.name.name.clone());
632 }
633 }
634 let found = found.or_else(|| run_bound_in_expr(&block.tail, at, bound));
635 bound.truncate(mark);
636 found
637}
638
639fn run_bound_in_expr(e: &Expr, at: usize, bound: &mut Vec<String>) -> Option<bool> {
640 if !span_covers(e.span, at) {
641 return None;
642 }
643 let mark = bound.len();
644 let found = match &e.kind {
645 ExprKind::Ident(id) if id.span.start == at && id.name == "run" => {
646 Some(bound.iter().any(|n| n == "run"))
647 }
648 ExprKind::Block(b) => run_bound_in_block(b, at, bound),
649 ExprKind::Lambda(l) => {
650 bound.extend(l.params.iter().map(|p| p.name.name.clone()));
651 run_bound_in_expr(&l.body, at, bound)
652 }
653 ExprKind::If {
654 cond,
655 then_block,
656 else_block,
657 } => run_bound_in_expr(cond, at, bound)
658 .or_else(|| {
659 let inner = bound.len();
660 bound.extend(is_bound_names(cond, true));
661 let r = run_bound_in_block(then_block, at, bound);
662 bound.truncate(inner);
663 r
664 })
665 .or_else(|| {
666 let inner = bound.len();
667 bound.extend(is_bound_names(cond, false));
668 let r = run_bound_in_block(else_block, at, bound);
669 bound.truncate(inner);
670 r
671 }),
672 ExprKind::BinOp(BinOp::And | BinOp::Implies, lhs, rhs) => run_bound_in_expr(lhs, at, bound)
674 .or_else(|| {
675 bound.extend(is_bound_names(lhs, true));
676 run_bound_in_expr(rhs, at, bound)
677 }),
678 ExprKind::Match { discriminant, arms } => run_bound_in_expr(discriminant, at, bound)
679 .or_else(|| {
680 arms.iter()
681 .find(|a| span_covers(a.span, at))
682 .and_then(|arm| {
683 bound.extend(
684 arm.pattern
685 .bound_names()
686 .into_iter()
687 .map(|id| id.name.clone()),
688 );
689 arm.guard
690 .as_ref()
691 .and_then(|g| run_bound_in_expr(g, at, bound))
692 .or_else(|| match &arm.body {
693 MatchBody::Expr(body) => run_bound_in_expr(body, at, bound),
694 MatchBody::Block(b) => run_bound_in_block(b, at, bound),
695 })
696 })
697 }),
698 _ => expr_children(e)
699 .into_iter()
700 .find_map(|c| run_bound_in_expr(c, at, bound)),
701 };
702 bound.truncate(mark);
703 found
704}
705
706fn is_bound_names(cond: &Expr, when_true: bool) -> Vec<String> {
709 bynk_check::narrowing::matched_is_tests(cond, when_true)
710 .into_iter()
711 .filter_map(|test| match &test.kind {
712 ExprKind::Is { pattern, .. } => Some(pattern.bound_names()),
713 _ => None,
714 })
715 .flatten()
716 .map(|id| id.name.clone())
717 .collect()
718}
719
720pub fn describe_run_id_at(source: &str, offset: usize) -> Option<String> {
724 let is_ident = |c: char| c.is_ascii_alphanumeric() || c == '_';
727 let offset = offset.min(source.len());
728 let start = source[..offset]
729 .char_indices()
730 .rev()
731 .take_while(|(_, c)| is_ident(*c))
732 .last()
733 .map_or(offset, |(i, _)| i);
734 let end = source[start..]
735 .char_indices()
736 .find(|(_, c)| !is_ident(*c))
737 .map_or(source.len(), |(i, _)| start + i);
738 let ends_ident = |rest: &str| !rest.starts_with(is_ident);
739 let run = match &source[start..end] {
740 "run" if source[end..].strip_prefix(".id").is_some_and(ends_ident) => start,
741 "id" if source[..start].ends_with("run.") => start - "run.".len(),
742 _ => return None,
743 };
744 if source[..run].ends_with(|c: char| is_ident(c) || c == '.') {
745 return None; }
747 is_test_run_receiver(source, run).then(|| RUN_ID_DOC.to_string())
748}
749
750pub fn describe_system_job_address_at<'s>(
759 source: &'s str,
760 offset: usize,
761 others: impl IntoIterator<Item = &'s str>,
762) -> Option<String> {
763 let is_ident = |c: char| c.is_ascii_alphanumeric() || c == '_';
764 let start = source[..offset.min(source.len())]
765 .char_indices()
766 .rev()
767 .take_while(|(_, c)| is_ident(*c))
768 .last()
769 .map_or(offset, |(i, _)| i);
770 let end = source[start..]
771 .char_indices()
772 .find(|(_, c)| !is_ident(*c))
773 .map_or(source.len(), |(i, _)| start + i);
774 let name = &source[start..end];
775 if !source[..start].ends_with('.') || !matches!(name, "schedule" | "message") {
776 return None;
777 }
778 let tokens = tokenize(source).ok()?;
779 let (unit, _errs) = parse_unit_with_recovery(&tokens, source);
780 let SourceUnit::Suite(suite) = unit? else {
781 return None;
782 };
783 let case = suite.cases.iter().find(|c| span_covers(c.span, start))?;
784 if case.tier.or(suite.tier) != Some(TestTier::System) {
785 return None;
786 }
787 let before = &source[..start - 1];
789 let receiver = &before[before
790 .char_indices()
791 .rev()
792 .take_while(|(_, c)| is_ident(*c))
793 .last()
794 .map_or(before.len(), |(i, _)| i)..];
795 let target = suite.target.joined();
796 let is_job_service = std::iter::once(source).chain(others).any(|src| {
797 let Ok(tokens) = tokenize(src) else {
798 return false;
799 };
800 let (unit, _errs) = parse_unit_with_recovery(&tokens, src);
801 let Some(SourceUnit::Context(ctx)) = unit else {
802 return false;
803 };
804 ctx.name.joined() == target
805 && ctx.items.iter().any(|item| {
806 matches!(item, CommonsItem::Service(svc)
807 if svc.name.name == receiver
808 && match name {
809 "schedule" => matches!(svc.protocol, ServiceProtocol::Cron),
810 _ => matches!(svc.protocol, ServiceProtocol::Queue { .. }),
811 })
812 })
813 });
814 if !is_job_service {
815 return None;
816 }
817 Some(if name == "schedule" {
818 "**`svc.schedule(\"<expr>\", at)`** at `system` — runs the cron handler through the Worker's `scheduled` entry, with `{ cron, scheduledTime: at }`, and yields the handler's own `Result`.".to_string()
819 } else {
820 "**`q.message(m)`** at `system` — delivers `m` through the Worker's `queue` entry, after a JSON round trip, and yields `Ack` or `Retry(reason)` as the consumer acknowledged or retried it.".to_string()
821 })
822}
823
824pub fn handler_annotation_token_spans(source: &str) -> Vec<Span> {
830 let Ok(tokens) = tokenize(source) else {
831 return Vec::new();
832 };
833 let (unit, _errs) = parse_unit_with_recovery(&tokens, source);
834 let Some(unit) = unit else {
835 return Vec::new();
836 };
837 let items: &[CommonsItem] = match &unit {
838 SourceUnit::Commons(c) => &c.items,
839 SourceUnit::Context(c) => &c.items,
840 SourceUnit::Adapter(a) => &a.items,
841 SourceUnit::Suite(_) => &[],
842 };
843 let mut spans = Vec::new();
844 for item in items {
845 let handlers: &[Handler] = match item {
846 CommonsItem::Service(s) => &s.handlers,
847 CommonsItem::Agent(a) => &a.handlers,
848 _ => continue,
849 };
850 for h in handlers {
851 for ann in &h.annotations {
852 spans.push(Span {
854 file: ann.span.file,
855 start: ann.span.start,
856 end: ann.name.span.end,
857 });
858 for arg in &ann.args {
860 if let Some(label) = &arg.label {
861 spans.push(label.span);
862 }
863 }
864 }
865 }
866 }
867 spans
868}
869
870fn cache_control_of(ann: &Annotation) -> Option<String> {
875 let mut max_age_secs = None;
876 let mut scope = "private";
877 let mut immutable = false;
878 for arg in &ann.args {
879 match (arg.label.as_ref().map(|l| l.name.as_str()), &arg.value.kind) {
880 (Some("maxAge"), ExprKind::DurationLit { millis, .. })
881 if *millis > 0 && millis % 1000 == 0 =>
882 {
883 max_age_secs = Some(millis / 1000);
884 }
885 (Some("scope"), ExprKind::Ident(id)) if id.name == "public" => scope = "public",
886 (Some("immutable"), ExprKind::BoolLit(b)) => immutable = *b,
887 _ => {}
888 }
889 }
890 let secs = max_age_secs?;
891 let suffix = if immutable { ", immutable" } else { "" };
892 Some(format!("{scope}, max-age={secs}{suffix}"))
893}
894
895fn render_handler_annotation_hover(
900 ann: &Annotation,
901 handler: &Handler,
902 service_cap: Option<i64>,
903) -> String {
904 let sig = bynk_fmt::annotation_to_string(ann);
905 if ann.name.name == "cache" {
906 let emitted = match cache_control_of(ann) {
907 Some(header) => format!("\n\nEmits `Cache-Control: {header}`."),
908 None => String::new(),
909 };
910 return format!(
911 "```bynk\n{sig}\n```\n\n\
912 **`@cache`** — cache this `GET` read. A `GET` returning `Ok` (or a `Raw` body \
913 of at most 1 MiB) already carries a synthesised weak `ETag`, and `RawTagged` \
914 carries the handler's own; a matching `If-None-Match` is answered \
915 `304 Not Modified`. `@cache` adds a `Cache-Control` freshness window on top.\n\n\
916 - **`maxAge`** — the freshness window, a `Duration` (e.g. `5.minutes`), lowered to \
917 `Cache-Control: max-age`.\n\
918 - **`scope`** — `public` or `private` (default `private`; a shared cache stores the \
919 response only when `public`).\n\
920 - **`immutable`** — `true` promises the response at this URL never changes, so \
921 caches do not revalidate it within `maxAge` (default `false`).{emitted}"
922 );
923 }
924 if ann.name.name == "limit" {
926 return format!(
927 "```bynk\n{sig}\n```\n\n\
928 **`@limit`** — cap the request body size on this `POST`/`PUT`/`PATCH` route. A \
929 request whose body exceeds the `maxBody` byte ceiling is answered `413 Payload Too \
930 Large`, synthesised before the body is read.\n\n\
931 - **`maxBody`** — the maximum request body size in bytes, a positive `Int`."
932 );
933 }
934 if ann.name.name == "raw" {
936 let cap = match handler.limit_max_body().or(service_cap) {
937 Some(bytes) => format!("{bytes} bytes (from `@limit` or the service `limits`)"),
938 None => "none — a `@raw` route needs one (`bynk.http.raw_body_unbounded`)".to_string(),
939 };
940 let content_type = match handler.raw_content_type() {
941 Some("*/*") => "any".to_string(),
942 Some(ct) => format!("`{ct}`"),
943 None => "—".to_string(),
944 };
945 return format!(
946 "```bynk\n{sig}\n```\n\n\
947 **`@raw`** — take this route's `body: Bytes` as the raw request body: the octets as \
948 sent, with no JSON and no base64. Any other `Content-Type` is answered `415 \
949 Unsupported Media Type`.\n\n\
950 - **`contentType`** — the media type accepted: {content_type}.\n\
951 - **Effective body cap** — {cap}."
952 );
953 }
954 format!("```bynk\n{sig}\n```")
955}
956
957fn span_covers(span: Span, offset: usize) -> bool {
959 span.start <= offset && offset < span.end
960}
961
962fn preceding_ident_span(
967 tokens: &[bynk_syntax::lexer::Token],
968 source: &str,
969 name_span: Span,
970 kw: &str,
971) -> Option<Span> {
972 let idx = tokens
973 .iter()
974 .position(|t| t.span.start == name_span.start)?;
975 let prev = tokens.get(idx.checked_sub(1)?)?;
976 (source.get(prev.span.start..prev.span.end) == Some(kw)).then_some(prev.span)
977}
978
979fn render_state_hover(sig: &str, contextual_kw: &str) -> String {
982 let doc = bynk_syntax::keywords::CONTEXTUAL_KEYWORDS
983 .iter()
984 .find(|k| k.word == contextual_kw)
985 .map(|k| k.meaning)
986 .unwrap_or_default();
987 format!("```bynk\n{sig}\n```\n\n{doc}")
988}
989
990pub fn describe_self_at(
1001 text: &str,
1002 offset: usize,
1003 expr_types: &[(Span, bynk_check::checker::TyId)],
1004 tys: &bynk_check::checker::Types,
1005) -> Option<String> {
1006 let tokens = tokenize(text).ok()?;
1007 let on_self = tokens.iter().any(|t| {
1008 t.span.start <= offset && offset < t.span.end && &text[t.span.start..t.span.end] == "self"
1009 });
1010 if !on_self {
1011 return None;
1012 }
1013 let ty = bynk_check::expr_types::type_at_offset(expr_types, offset, tys)?;
1014 let display = ty.display(tys);
1015 let name = display
1016 .strip_prefix("__")
1017 .and_then(|s| s.strip_suffix("Self"))
1018 .unwrap_or(&display);
1019 Some(format!("```bynk\nself: {name}\n```"))
1020}
1021
1022pub fn qualified_callee_at(text: &str, ident_span: Span) -> Option<String> {
1030 let (recv, _) = receiver_segment_at(text, ident_span)?;
1031 if !recv.chars().next()?.is_uppercase() {
1032 return None;
1033 }
1034 let member = text.get(ident_span.start..ident_span.end)?;
1035 Some(format!("{recv}.{member}"))
1036}
1037
1038pub fn describe_firstparty_symbol(name: &str) -> Option<String> {
1046 bynk_check::firstparty::FIRSTPARTY_SOURCES
1049 .iter()
1050 .find_map(|(unit, src)| {
1051 describe_symbol(src, name).map(|d| d + &firstparty_notes(unit, name))
1052 })
1053}
1054
1055pub fn firstparty_notes(unit: &str, name: &str) -> String {
1060 let mut out = String::new();
1061 let path = format!("{unit}.{name}");
1062 if bynk_check::firstparty::stability_of(&path)
1063 == Some(bynk_check::firstparty::Stability::Experimental)
1064 {
1065 out.push_str(&format!(
1066 "\n\n**Experimental**: it may change or disappear in any release. To use it, add `\"{path}\"` to `[unstable] allow` in `bynk.toml`."
1067 ));
1068 }
1069 if bynk_check::firstparty::is_extensible(unit, name) {
1070 out.push_str(
1071 "\n\n**Extensible**: it may gain variants in a minor release, so a `match` on it needs a wildcard arm.",
1072 );
1073 }
1074 out
1075}
1076
1077pub fn unit_reference_spans(source: &str) -> Vec<(String, Span)> {
1082 let Ok(tokens) = tokenize(source) else {
1083 return Vec::new();
1084 };
1085 let (Some(unit), _) = parse_unit_with_recovery(&tokens, source) else {
1086 return Vec::new();
1087 };
1088 let mut out: Vec<(String, Span)> = Vec::new();
1089 let (uses, consumes): (&[UsesDecl], &[ConsumesDecl]) = match &unit {
1092 SourceUnit::Commons(c) => (&c.uses, &[]),
1093 SourceUnit::Context(c) => (&c.uses, &c.consumes),
1094 SourceUnit::Adapter(a) => (&a.uses, &a.consumes),
1095 SourceUnit::Suite(s) => {
1096 out.push((s.target.joined(), s.target.span));
1097 (&s.uses, &[])
1098 }
1099 };
1100 for u in uses {
1101 out.push((u.target.joined(), u.target.span));
1102 }
1103 for c in consumes {
1104 out.push((c.target.joined(), c.target.span));
1105 }
1106 out
1107}
1108
1109pub fn unit_name_at(source: &str, offset: usize) -> Option<String> {
1118 let own = own_declaration_name(source);
1119 own.into_iter()
1120 .chain(unit_reference_spans(source))
1121 .find(|(_, span)| span.start <= offset && offset <= span.end)
1122 .map(|(name, _)| name)
1123}
1124
1125pub fn describe_unit<'s>(sources: impl IntoIterator<Item = &'s str>, name: &str) -> Option<String> {
1137 let mut kind: Option<&'static str> = None;
1138 let mut doc: Option<String> = None;
1139 for source in sources {
1140 let Ok(tokens) = tokenize(source) else {
1141 continue;
1142 };
1143 let (Some(unit), _) = parse_unit_with_recovery(&tokens, source) else {
1144 continue;
1145 };
1146 if matches!(unit, SourceUnit::Suite(_)) || unit.name().joined() != name {
1147 continue;
1148 }
1149 kind.get_or_insert(unit.kind_name());
1150 if doc.is_none() {
1151 doc = unit.module_doc().map(str::to_string);
1152 }
1153 if doc.is_some() {
1154 break;
1155 }
1156 }
1157 let kind = kind?;
1158 let mut out = format!("```bynk\n{kind} {name}\n```\n");
1159 if let Some(doc) = doc {
1160 out.push('\n');
1161 out.push_str(&doc);
1162 out.push('\n');
1163 }
1164 Some(out)
1165}
1166
1167pub fn describe_unit_at<'s>(
1173 text: &'s str,
1174 offset: usize,
1175 others: impl IntoIterator<Item = &'s str>,
1176) -> Option<String> {
1177 let name = unit_name_at(text, offset)?;
1178 let firstparty = bynk_check::firstparty::FIRSTPARTY_SOURCES
1179 .iter()
1180 .map(|(_, src)| *src);
1181 describe_unit(std::iter::once(text).chain(others).chain(firstparty), &name)
1182}
1183
1184#[derive(Debug, Clone, PartialEq, Eq)]
1191pub struct DocLinkCandidate {
1192 pub name: String,
1193 pub span: std::ops::Range<usize>,
1194 pub display: String,
1195}
1196
1197fn is_doc_link_name(s: &str) -> bool {
1201 fn is_ident(part: &str) -> bool {
1202 let mut chars = part.chars();
1203 matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_')
1204 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
1205 }
1206 match s.split_once('.') {
1207 Some((owner, member)) => is_ident(owner) && is_ident(member),
1208 None => is_ident(s),
1209 }
1210}
1211
1212fn doc_link_prepass(text: &str) -> (Vec<(usize, usize)>, std::collections::HashSet<String>) {
1221 let mut fenced = Vec::new();
1222 let mut defined = std::collections::HashSet::new();
1223 let mut in_fence = false;
1224 let mut fence_start = 0usize;
1225 let mut pos = 0usize;
1226 loop {
1227 let line_end = text[pos..]
1228 .find('\n')
1229 .map(|i| pos + i + 1)
1230 .unwrap_or(text.len());
1231 let line = &text[pos..line_end];
1232 let trimmed = line.trim_start();
1233 if trimmed.starts_with("```") {
1234 if in_fence {
1235 fenced.push((fence_start, line_end));
1236 in_fence = false;
1237 } else {
1238 fence_start = pos;
1239 in_fence = true;
1240 }
1241 } else if !in_fence {
1242 let indent = line.len() - trimmed.len();
1245 if indent <= 3
1246 && let Some(rest) = trimmed.strip_prefix('[')
1247 && let Some(close) = rest.find(']')
1248 {
1249 let label = &rest[..close];
1250 if let Some(after_colon) = rest[close + 1..].strip_prefix(':')
1251 && !label.is_empty()
1252 && !after_colon.trim().is_empty()
1253 {
1254 defined.insert(label.to_string());
1255 }
1256 }
1257 }
1258 if line_end >= text.len() {
1259 break;
1260 }
1261 pos = line_end;
1262 }
1263 if in_fence {
1264 fenced.push((fence_start, text.len()));
1265 }
1266 (fenced, defined)
1267}
1268
1269fn scan_bracket_candidate(
1275 text: &str,
1276 start: usize,
1277 defined: &std::collections::HashSet<String>,
1278) -> Option<DocLinkCandidate> {
1279 let bytes = text.as_bytes();
1280 debug_assert_eq!(bytes[start], b'[');
1281 if bytes.get(start + 1) == Some(&b'`') {
1283 let inner_start = start + 2;
1284 let close_tick = text[inner_start..].find('`')? + inner_start;
1285 if bytes.get(close_tick + 1) != Some(&b']') {
1286 return None;
1287 }
1288 let name = &text[inner_start..close_tick];
1289 if !is_doc_link_name(name) || defined.contains(name) {
1290 return None;
1291 }
1292 return Some(DocLinkCandidate {
1293 name: name.to_string(),
1294 span: start..close_tick + 2,
1295 display: text[start + 1..close_tick + 1].to_string(),
1296 });
1297 }
1298 let inner_start = start + 1;
1299 let close = text[inner_start..].find(']')? + inner_start;
1300 let inner = &text[inner_start..close];
1301 let after = close + 1;
1302 match bytes.get(after) {
1303 Some(b'(') => None,
1305 Some(b':') => None,
1308 Some(b'[') => {
1310 let label_start = after + 1;
1311 let label_close = text[label_start..].find(']')? + label_start;
1312 let label = &text[label_start..label_close];
1313 if label.is_empty() || !is_doc_link_name(label) || defined.contains(label) {
1315 return None;
1316 }
1317 Some(DocLinkCandidate {
1318 name: label.to_string(),
1319 span: start..label_close + 1,
1320 display: inner.to_string(),
1321 })
1322 }
1323 _ => {
1325 if !is_doc_link_name(inner) || defined.contains(inner) {
1326 return None;
1327 }
1328 Some(DocLinkCandidate {
1329 name: inner.to_string(),
1330 span: start..after,
1331 display: inner.to_string(),
1332 })
1333 }
1334 }
1335}
1336
1337pub fn scan_doc_link_candidates(text: &str) -> Vec<DocLinkCandidate> {
1342 let (fenced, defined) = doc_link_prepass(text);
1343 let in_fenced = |i: usize| fenced.iter().any(|&(s, e)| i >= s && i < e);
1344 let bytes = text.as_bytes();
1345 let mut out = Vec::new();
1346 let mut i = 0usize;
1347 while i < bytes.len() {
1348 if bytes[i] != b'[' || in_fenced(i) {
1349 i += 1;
1350 continue;
1351 }
1352 match scan_bracket_candidate(text, i, &defined) {
1353 Some(cand) => {
1354 i = cand.span.end;
1355 out.push(cand);
1356 }
1357 None => i += 1,
1358 }
1359 }
1360 out
1361}
1362
1363pub fn doc_link_spans(source: &str) -> Vec<(String, Span)> {
1373 let Ok(tokens) = tokenize(source) else {
1374 return Vec::new();
1375 };
1376 let mut out = Vec::new();
1377 for t in tokens {
1378 let range = match t.kind {
1380 bynk_syntax::lexer::TokenKind::DocBlock => {
1381 bynk_syntax::lexer::doc_block_body_range(source, t.span)
1382 }
1383 bynk_syntax::lexer::TokenKind::DocLine => {
1384 Some(bynk_syntax::lexer::doc_line_text_range(source, t.span))
1385 }
1386 _ => None,
1387 };
1388 let Some(range) = range else {
1389 continue;
1390 };
1391 for cand in scan_doc_link_candidates(&source[range.clone()]) {
1392 out.push((
1393 cand.name,
1394 Span::new(range.start + cand.span.start, range.start + cand.span.end),
1395 ));
1396 }
1397 }
1398 out
1399}
1400
1401pub fn own_declaration_name(source: &str) -> Option<(String, Span)> {
1406 let tokens = tokenize(source).ok()?;
1407 let (Some(unit), _) = parse_unit_with_recovery(&tokens, source) else {
1408 return None;
1409 };
1410 if matches!(unit, SourceUnit::Suite(_)) {
1411 return None;
1412 }
1413 let name = unit.name();
1414 Some((name.joined(), name.span))
1415}
1416
1417fn describe_item(item: &CommonsItem, name: &str) -> Option<String> {
1418 match item {
1419 CommonsItem::Type(t) if t.name.name == name => Some(describe_type(t)),
1420 CommonsItem::Fn(f) if matches!(f.name, FnName::Free(_)) && f.name.ident().name == name => {
1427 Some(describe_fn(f))
1428 }
1429 CommonsItem::Capability(c) if c.name.name == name => Some(describe_capability(c)),
1430 CommonsItem::Service(s) if s.name.name == name => Some(describe_service(s)),
1431 CommonsItem::Agent(a) if a.name.name == name => Some(describe_agent(a)),
1432 CommonsItem::Provider(p) if p.provider_name.name == name => Some(describe_provider(p)),
1433 CommonsItem::Actor(a) if a.name.name == name => Some(describe_actor(a)),
1438 CommonsItem::Messages(m) if m.tag == name => Some(describe_messages(m)),
1442 CommonsItem::Asset(a) if a.name.name == name => Some(describe_asset(a)),
1444 CommonsItem::Embed(e) if e.name.name == name => Some(describe_embed(e)),
1446 CommonsItem::Event(e) if e.name.name == name => Some(describe_event(e)),
1451 CommonsItem::Type(t) => {
1458 let (owner, field) = name.rsplit_once('.')?;
1459 if t.name.name != owner {
1460 return None;
1461 }
1462 let TypeBody::Record(r) = &t.body else {
1463 return None;
1464 };
1465 r.fields
1466 .iter()
1467 .find(|f| f.name.name == field)
1468 .map(|f| describe_record_field(t, f))
1469 }
1470 CommonsItem::Event(e) => {
1475 let (owner, field) = name.rsplit_once('.')?;
1476 if e.name.name != owner {
1477 return None;
1478 }
1479 let synthetic = e.as_type_decl();
1480 e.body
1481 .fields
1482 .iter()
1483 .find(|f| f.name.name == field)
1484 .map(|f| describe_record_field(&synthetic, f))
1485 }
1486 CommonsItem::Fn(f) => (f.name.display() == name).then(|| describe_fn(f)),
1491 CommonsItem::Capability(c) => {
1493 let (owner, op) = name.rsplit_once('.')?;
1494 if c.name.name != owner {
1495 return None;
1496 }
1497 c.ops
1498 .iter()
1499 .find(|o| o.name.name == op)
1500 .map(|o| describe_capability_op(c, o))
1501 }
1502 CommonsItem::Agent(a) => {
1506 let (owner, handler) = name.rsplit_once('.')?;
1507 if a.name.name != owner {
1508 return None;
1509 }
1510 a.handlers
1511 .iter()
1512 .find(|h| h.method_name.as_ref().is_some_and(|n| n.name == handler))
1513 .map(|h| describe_agent_handler(a, h, handler))
1514 }
1515 _ => None,
1516 }
1517}
1518
1519pub(crate) fn describe_actor(a: &ActorDecl) -> String {
1527 let mut out = String::from("```bynk\n");
1528 match &a.refinement {
1529 Some(r) => out.push_str(&format!(
1531 "actor {} = {} where {}",
1532 a.name.name,
1533 r.base.name,
1534 bynk_fmt::expr_to_string(&r.predicate)
1535 )),
1536 None => {
1537 let auth = a.auth.as_ref().map_or("None", |i| i.name.as_str());
1539 out.push_str(&format!("actor {} {{ auth = {auth}", a.name.name));
1540 if !a.auth_config.is_empty() {
1541 let args: Vec<String> = a
1542 .auth_config
1543 .iter()
1544 .map(|arg| match &arg.value {
1545 SchemeArgValue::Str(s) => {
1550 format!("{} = \"{}\"", arg.key.name, bynk_fmt::escape_string(s))
1551 }
1552 SchemeArgValue::Int(n) => format!("{} = {n}", arg.key.name),
1553 })
1554 .collect();
1555 out.push_str(&format!("({})", args.join(", ")));
1556 }
1557 if let Some(id) = &a.identity {
1558 out.push_str(&format!(", identity = {}", type_ref_str(id)));
1559 }
1560 out.push_str(" }");
1561 }
1562 }
1563 out.push_str("\n```\n");
1564 if let Some(doc) = &a.documentation {
1565 out.push('\n');
1566 out.push_str(doc);
1567 out.push('\n');
1568 }
1569 out
1570}
1571
1572fn capability_op_type_params_str(op: &CapabilityOp) -> String {
1576 if op.type_params.is_empty() {
1577 return String::new();
1578 }
1579 let names: Vec<&str> = op
1580 .type_params
1581 .iter()
1582 .map(|tp| tp.name.name.as_str())
1583 .collect();
1584 format!("[{}]", names.join(", "))
1585}
1586
1587pub(crate) fn describe_capability_op(c: &CapabilityDecl, op: &CapabilityOp) -> String {
1591 let params: Vec<String> = op
1592 .params
1593 .iter()
1594 .map(|p| format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)))
1595 .collect();
1596 let mut out = format!(
1597 "```bynk\nfn {}{}({}) -> {}\n```\n\nAn operation of capability `{}`.\n",
1598 op.name.name,
1599 capability_op_type_params_str(op),
1600 params.join(", "),
1601 type_ref_str(&op.return_type),
1602 c.name.name
1603 );
1604 if let Some(doc) = &op.documentation {
1605 out.push('\n');
1606 out.push_str(doc);
1607 out.push('\n');
1608 }
1609 out
1610}
1611
1612pub(crate) fn describe_agent_handler(a: &AgentDecl, h: &Handler, handler_name: &str) -> String {
1615 let params: Vec<String> = h
1616 .params
1617 .iter()
1618 .map(|p| format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)))
1619 .collect();
1620 let mut out = format!(
1621 "```bynk\nfn {}({}) -> {}\n```\n\nA handler of agent `{}`.\n",
1622 handler_name,
1623 params.join(", "),
1624 type_ref_str(&h.return_type),
1625 a.name.name
1626 );
1627 if let Some(doc) = &h.documentation {
1628 out.push('\n');
1629 out.push_str(doc);
1630 out.push('\n');
1631 }
1632 out
1633}
1634
1635pub(crate) fn describe_service_handler(s: &ServiceDecl, h: &Handler) -> String {
1644 let params: Vec<String> = h
1645 .params
1646 .iter()
1647 .map(|p| format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)))
1648 .collect();
1649 let mut out = format!(
1650 "```bynk\n{}({}) -> {}\n```\n\nA handler of service `{}`.\n",
1651 handler_line(h),
1652 params.join(", "),
1653 type_ref_str(&h.return_type),
1654 s.name.name
1655 );
1656 if let Some(doc) = &h.documentation {
1657 out.push('\n');
1658 out.push_str(doc);
1659 out.push('\n');
1660 }
1661 out
1662}
1663
1664pub(crate) fn describe_record_field(t: &TypeDecl, f: &RecordField) -> String {
1671 let mut sig = format!("{}: {}", f.name.name, type_ref_str(&f.type_ref));
1672 if let Some(r) = &f.refinement {
1673 sig.push_str(&format!(" where {}", bynk_fmt::refinement_to_string(r)));
1674 }
1675 if let Some(init) = &f.init {
1679 sig.push_str(&format!(" = {}", bynk_fmt::expr_to_string(init)));
1680 }
1681 format!(
1682 "```bynk\n{sig}\n```\n\n{}",
1683 member_prose(&f.documentation, || format!(
1684 "A field of `{}`.",
1685 t.name.name
1686 ))
1687 )
1688}
1689
1690fn member_prose(doc: &Option<Doc>, fallback: impl FnOnce() -> String) -> String {
1693 match doc {
1694 Some(d) => d.trim_end().to_string(),
1695 None => fallback(),
1696 }
1697}
1698
1699pub(crate) fn describe_variant(t: &TypeDecl, v: &Variant) -> String {
1703 let mut sig = format!("| {}", v.name.name);
1704 if !v.payload.is_empty() {
1705 let parts: Vec<String> = v
1706 .payload
1707 .iter()
1708 .map(|p| format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)))
1709 .collect();
1710 sig.push_str(&format!("({})", parts.join(", ")));
1711 }
1712 let mut out = format!(
1713 "```bynk\n{sig}\n```\n\n{}",
1714 member_prose(&v.documentation, || format!(
1715 "A variant of `{}`.",
1716 t.name.name
1717 ))
1718 );
1719 for p in &v.payload {
1720 if let Some(doc) = &p.documentation {
1721 out.push_str(&format!("\n\n`{}`: {}", p.name.name, doc.trim_end()));
1722 }
1723 }
1724 out
1725}
1726
1727pub(crate) fn describe_variant_field(t: &TypeDecl, v: &Variant, p: &VariantField) -> String {
1730 format!(
1731 "```bynk\n{}: {}\n```\n\n{}",
1732 p.name.name,
1733 type_ref_str(&p.type_ref),
1734 member_prose(&p.documentation, || format!(
1735 "A field of `{}.{}`.",
1736 t.name.name, v.name.name
1737 ))
1738 )
1739}
1740
1741fn with_sum_variants<R>(
1743 source: &str,
1744 mut f: impl FnMut(&TypeDecl, &Variant) -> Option<R>,
1745) -> Option<R> {
1746 let tokens = tokenize(source).ok()?;
1747 let (unit, _errs) = parse_unit_with_recovery(&tokens, source);
1748 let items: &[CommonsItem] = match unit.as_ref()? {
1749 SourceUnit::Commons(c) => &c.items,
1750 SourceUnit::Context(c) => &c.items,
1751 SourceUnit::Adapter(a) => &a.items,
1752 SourceUnit::Suite(_) => &[],
1753 };
1754 for item in items {
1755 if let CommonsItem::Type(t) = item
1756 && let TypeBody::Sum(s) = &t.body
1757 {
1758 for v in &s.variants {
1759 if let Some(r) = f(t, v) {
1760 return Some(r);
1761 }
1762 }
1763 }
1764 }
1765 None
1766}
1767
1768pub fn describe_variant_key(source: &str, key: &str) -> Option<String> {
1771 let (owner, name) = key.rsplit_once('.')?;
1772 with_sum_variants(source, |t, v| {
1773 (t.name.name == owner && v.name.name == name).then(|| describe_variant(t, v))
1774 })
1775}
1776
1777pub fn describe_variant_named(source: &str, name: &str) -> Option<String> {
1782 let mut found: Vec<String> = Vec::new();
1783 with_sum_variants::<()>(source, |t, v| {
1784 if v.name.name == name {
1785 found.push(describe_variant(t, v));
1786 }
1787 None
1788 });
1789 (found.len() == 1).then(|| found.remove(0))
1790}
1791
1792pub(crate) fn describe_type(t: &TypeDecl) -> String {
1793 let mut out = String::new();
1794 out.push_str("```bynk\n");
1795 let params = if t.type_params.is_empty() {
1797 String::new()
1798 } else {
1799 let names: Vec<&str> = t
1800 .type_params
1801 .iter()
1802 .map(|tp| tp.name.name.as_str())
1803 .collect();
1804 format!("[{}]", names.join(", "))
1805 };
1806 out.push_str(&format!("type {}{} = ", t.name.name, params));
1807 match &t.body {
1808 TypeBody::Refined {
1811 base, refinement, ..
1812 } => {
1813 out.push_str(base.name());
1814 if let Some(r) = refinement {
1815 out.push_str(&format!(" where {}", bynk_fmt::refinement_to_string(r)));
1816 }
1817 }
1818 TypeBody::Opaque {
1819 base, refinement, ..
1820 } => {
1821 out.push_str(&format!("opaque {}", base.name()));
1822 if let Some(r) = refinement {
1823 out.push_str(&format!(" where {}", bynk_fmt::refinement_to_string(r)));
1824 }
1825 }
1826 TypeBody::Record(r) => {
1828 if r.fields.is_empty() {
1829 out.push_str("{}");
1830 } else {
1831 out.push_str("{\n");
1832 for f in &r.fields {
1833 out.push_str(&format!("\t{}: {}", f.name.name, type_ref_str(&f.type_ref)));
1834 if let Some(r) = &f.refinement {
1835 out.push_str(&format!(" where {}", bynk_fmt::refinement_to_string(r)));
1836 }
1837 if let Some(init) = &f.init {
1841 out.push_str(&format!(" = {}", bynk_fmt::expr_to_string(init)));
1842 }
1843 out.push_str(",\n");
1844 }
1845 out.push('}');
1846 }
1847 }
1848 TypeBody::Sum(s) => {
1850 out.push_str("enum {\n");
1851 for v in &s.variants {
1852 out.push_str(&format!("\t{}", v.name.name));
1853 if !v.payload.is_empty() {
1854 let parts: Vec<String> = v
1855 .payload
1856 .iter()
1857 .map(|p| format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)))
1858 .collect();
1859 out.push_str(&format!("({})", parts.join(", ")));
1860 }
1861 out.push_str(",\n");
1862 }
1863 out.push('}');
1864 }
1865 }
1866 out.push_str("\n```\n");
1867 if let Some(doc) = &t.documentation {
1868 out.push('\n');
1869 out.push_str(doc);
1870 out.push('\n');
1871 }
1872 out
1873}
1874
1875fn describe_event(e: &EventDecl) -> String {
1883 let base = describe_type(&e.as_type_decl());
1884 if e.annotations.is_empty() {
1885 return base;
1886 }
1887 let annotations: String = e
1888 .annotations
1889 .iter()
1890 .map(|a| format!(" {}", bynk_fmt::annotation_to_string(a)))
1891 .collect();
1892 let from = format!("type {} = ", e.name.name);
1893 let to = format!("type {}{annotations} = ", e.name.name);
1894 base.replacen(&from, &to, 1)
1895}
1896
1897pub(crate) fn describe_fn(f: &FnDecl) -> String {
1898 let mut out = String::new();
1899 out.push_str("```bynk\n");
1900 out.push_str("fn ");
1901 out.push_str(&f.name.display());
1902 out.push('(');
1903 let mut parts: Vec<String> = Vec::new();
1904 if f.has_self {
1905 parts.push("self".into());
1906 }
1907 for p in &f.params {
1908 parts.push(format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)));
1909 }
1910 out.push_str(&parts.join(", "));
1911 out.push_str(") -> ");
1912 out.push_str(&type_ref_str(&f.return_type));
1913 for c in &f.requires {
1916 out.push_str(&format!(
1917 "\n\trequires {}: {}",
1918 c.name.name,
1919 bynk_fmt::expr_to_string(&c.predicate)
1920 ));
1921 }
1922 for c in &f.ensures {
1923 out.push_str(&format!(
1924 "\n\tensures {}: {}",
1925 c.name.name,
1926 bynk_fmt::expr_to_string(&c.predicate)
1927 ));
1928 }
1929 out.push_str("\n```\n");
1930 if let Some(doc) = &f.documentation {
1931 out.push('\n');
1932 out.push_str(doc);
1933 out.push('\n');
1934 }
1935 out
1936}
1937
1938pub(crate) fn describe_capability(c: &CapabilityDecl) -> String {
1939 let mut out = String::new();
1940 if c.derived.is_some() {
1942 return describe_derived_capability(c, None, None);
1943 }
1944 out.push_str("```bynk\ncapability ");
1945 out.push_str(&c.name.name);
1946 out.push_str(" {\n");
1947 for op in &c.ops {
1948 out.push_str("\tfn ");
1949 out.push_str(&op.name.name);
1950 out.push_str(&capability_op_type_params_str(op));
1951 out.push('(');
1952 let parts: Vec<String> = op
1953 .params
1954 .iter()
1955 .map(|p| format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)))
1956 .collect();
1957 out.push_str(&parts.join(", "));
1958 out.push_str(") -> ");
1959 out.push_str(&type_ref_str(&op.return_type));
1960 out.push('\n');
1961 }
1962 out.push_str("}\n```\n");
1963 if let Some(doc) = &c.documentation {
1964 out.push('\n');
1965 out.push_str(doc);
1966 out.push('\n');
1967 }
1968 out
1969}
1970
1971pub(crate) fn describe_derived_capability(
1975 c: &CapabilityDecl,
1976 base_unit: Option<&str>,
1977 base: Option<&CapabilityDecl>,
1978) -> String {
1979 let Some(d) = &c.derived else {
1980 return describe_capability(c);
1981 };
1982 let mut out = format!("```bynk\ncapability {} = {}", c.name.name, d.base.name);
1983 for a in &d.annotations {
1984 out.push_str(" @");
1985 out.push_str(&a.name.name);
1986 }
1987 let from = base_unit.map_or(String::new(), |u| format!(" (`{u}`)"));
1988 let what = if d.base.name == "EdgeCache" {
1991 "this context's Worker's edge cache"
1992 } else {
1993 "its own storage"
1994 };
1995 out.push_str(&format!(
1996 "\n```\n\nA distinct instance of `{}`{from}: {what}, with `{}`'s operations",
1997 d.base.name, d.base.name
1998 ));
1999 match base {
2000 Some(b) if !b.ops.is_empty() => {
2001 out.push_str(":\n\n```bynk\n");
2002 for op in &b.ops {
2003 let params: Vec<String> = op
2004 .params
2005 .iter()
2006 .map(|p| format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)))
2007 .collect();
2008 out.push_str(&format!(
2009 "fn {}{}({}) -> {}\n",
2010 op.name.name,
2011 capability_op_type_params_str(op),
2012 params.join(", "),
2013 type_ref_str(&op.return_type)
2014 ));
2015 }
2016 out.push_str("```\n");
2017 }
2018 _ => out.push_str(".\n"),
2019 }
2020 if d.base.name == "RateLimit" {
2022 let number = |name: &str| {
2023 d.annotations
2024 .iter()
2025 .find(|a| a.name.name == name)
2026 .and_then(|a| a.args.first())
2027 .and_then(|arg| match &arg.value.kind {
2028 ExprKind::IntLit { value, .. } => Some(*value),
2029 ExprKind::DurationLit { millis, .. } => Some(millis / 1000),
2030 _ => None,
2031 })
2032 };
2033 if let (Some(limit), Some(period)) = (number("limit"), number("period")) {
2034 out.push_str(&format!(
2035 "\nEmits `[[ratelimits]]` with `simple = {{ limit = {limit}, period = {period} }}`: {limit} calls per key every {period} seconds. Counting is approximate (per Cloudflare location, eventually consistent).\n"
2036 ));
2037 }
2038 }
2039 if d.base.name == "EdgeCache" {
2040 out.push_str("\nEmits `[cache] enabled = true`: a `@cache` route's `2xx` is cached at Cloudflare's edge, and every other `GET` or `HEAD` answer is `no-store`.\n");
2041 }
2042 if let Some(doc) = &c.documentation {
2043 out.push('\n');
2044 out.push_str(doc);
2045 out.push('\n');
2046 }
2047 out
2048}
2049
2050pub(crate) fn describe_messages(m: &MessagesDecl) -> String {
2054 let mut out = String::new();
2055 out.push_str("```bynk\nmessages ");
2056 out.push('"');
2057 out.push_str(&m.tag);
2058 out.push('"');
2059 for ann in &m.annotations {
2060 out.push(' ');
2061 out.push_str(&bynk_fmt::annotation_to_string(ann));
2062 }
2063 out.push_str(" {\n");
2064 for entry in &m.entries {
2065 let icu_kinds: Vec<String> = bynk_check::icu::template_format_kinds(&entry.template)
2070 .into_iter()
2071 .map(|(name, kind)| (name.to_string(), kind.as_str()))
2072 .filter(|(_, kind)| *kind != "plain")
2073 .map(|(name, kind)| format!("{name}: {kind}"))
2074 .collect();
2075 if icu_kinds.is_empty() {
2076 out.push_str(&format!("\t\"{}\" => …\n", entry.code));
2077 } else {
2078 out.push_str(&format!(
2079 "\t\"{}\" => … // {}\n",
2080 entry.code,
2081 icu_kinds.join(", ")
2082 ));
2083 }
2084 }
2085 out.push_str("}\n```\n");
2086 if let Some(doc) = &m.documentation {
2087 out.push('\n');
2088 out.push_str(doc);
2089 out.push('\n');
2090 }
2091 out
2092}
2093
2094pub(crate) fn describe_embed(e: &EmbedDecl) -> String {
2099 let mut out = format!(
2100 "```bynk\nembed {} = \"{}\"\n```\n\nPrivate: the file's text, compiled into the code as a `String`; never served.\n",
2101 e.name.name,
2102 bynk_fmt::escape_string(&e.path)
2103 );
2104 if let Some(doc) = &e.documentation {
2105 out.push('\n');
2106 out.push_str(doc);
2107 out.push('\n');
2108 }
2109 out
2110}
2111
2112pub(crate) fn describe_asset(a: &AssetDecl) -> String {
2113 let at =
2114 a.at.as_ref()
2115 .map(|(p, _)| format!(" at \"{}\"", bynk_fmt::escape_string(p)))
2116 .unwrap_or_default();
2117 let served = match &a.at {
2122 Some((p, _)) => format!("Served at `{p}`."),
2123 None => format!(
2124 "Served at `{}` (content-hashed, cached as immutable).",
2125 bynk_check::assets::served_path_with_hash(
2126 &bynk_project::assets::normalise_asset_path(&a.path),
2127 "<hash>"
2128 )
2129 ),
2130 };
2131 let mut out = format!(
2132 "```bynk\nasset {} = \"{}\"{at}\n```\n\n{served} Public: served by the platform, never by the Worker.\n",
2133 a.name.name,
2134 bynk_fmt::escape_string(&a.path)
2135 );
2136 if let Some(doc) = &a.documentation {
2137 out.push('\n');
2138 out.push_str(doc);
2139 out.push('\n');
2140 }
2141 out
2142}
2143
2144fn service_protocol_suffix(p: &ServiceProtocol) -> String {
2147 match p {
2148 ServiceProtocol::Call => String::new(),
2149 ServiceProtocol::Http => " from http".to_string(),
2150 ServiceProtocol::Cron => " from cron".to_string(),
2151 ServiceProtocol::Queue { name } => format!(" from queue(\"{name}\")"),
2152 ServiceProtocol::WebSocket { .. } => " from websocket".to_string(),
2153 ServiceProtocol::Events { event_type, .. } => {
2158 format!(" from Events({})", type_ref_str(event_type))
2159 }
2160 }
2161}
2162
2163fn cors_summary(cors: &CorsPolicy) -> String {
2166 let mut parts = vec![format!("origins: {:?}", cors.origins())];
2167 if cors.credentials() {
2168 parts.push("credentials: true".to_string());
2169 }
2170 if let Some(secs) = cors.max_age_secs() {
2171 parts.push(format!("maxAge: {secs}s"));
2172 }
2173 parts.join(", ")
2174}
2175
2176fn security_summary(security: &SecurityPolicy) -> String {
2179 let mut parts = Vec::new();
2180 if !security.nosniff() {
2181 parts.push("nosniff: false".to_string());
2182 }
2183 if let Some(secs) = security.hsts_max_age_secs() {
2184 parts.push(format!("hsts: {secs}s"));
2185 }
2186 if parts.is_empty() {
2187 "nosniff".to_string()
2189 } else {
2190 parts.join(", ")
2191 }
2192}
2193
2194fn limits_summary(limits: &LimitsPolicy) -> String {
2197 match limits.max_body() {
2198 Some(bytes) => format!("maxBody: {bytes} bytes"),
2199 None => "maxBody".to_string(),
2200 }
2201}
2202
2203pub(crate) fn handler_line(h: &Handler) -> String {
2205 match &h.kind {
2206 HandlerKind::Call => "on call".to_string(),
2207 HandlerKind::Http { method, path } => format!("on {}(\"{}\")", method.as_str(), path),
2208 HandlerKind::Cron { expr } => format!("on schedule(\"{expr}\")"),
2209 HandlerKind::Message => "on message".to_string(),
2210 HandlerKind::Open => "on open".to_string(),
2211 HandlerKind::Close => "on close".to_string(),
2212 HandlerKind::Event => "on event".to_string(),
2213 HandlerKind::Rejected => "on rejected".to_string(),
2214 }
2215}
2216
2217pub(crate) fn describe_service(s: &ServiceDecl) -> String {
2218 let mut out = format!(
2221 "```bynk\nservice {}{} {{\n",
2222 s.name.name,
2223 service_protocol_suffix(&s.protocol)
2224 );
2225 if let Some(cors) = &s.cors {
2228 out.push_str(&format!("\tcors {{ {} }}\n", cors_summary(cors)));
2229 }
2230 if let Some(security) = &s.security {
2232 out.push_str(&format!(
2233 "\tsecurity {{ {} }}\n",
2234 security_summary(security)
2235 ));
2236 }
2237 if let Some(limits) = &s.limits {
2239 out.push_str(&format!("\tlimits {{ {} }}\n", limits_summary(limits)));
2240 }
2241 for h in &s.handlers {
2242 out.push_str(&format!("\t{}\n", handler_line(h)));
2243 }
2244 out.push_str("}\n```\n");
2245 if let Some(doc) = &s.documentation {
2246 out.push('\n');
2247 out.push_str(doc);
2248 out.push('\n');
2249 }
2250 out
2251}
2252
2253fn store_kind_str(k: &StoreKind) -> String {
2256 if k.args.is_empty() {
2257 k.head.name.clone()
2258 } else {
2259 let args: Vec<String> = k.args.iter().map(type_ref_str).collect();
2260 format!("{}[{}]", k.head.name, args.join(", "))
2261 }
2262}
2263
2264pub(crate) fn describe_agent(a: &AgentDecl) -> String {
2265 let mut out = format!(
2268 "```bynk\nagent {} {{\n\tkey {}: {}\n",
2269 a.name.name,
2270 a.key_name.name,
2271 type_ref_str(&a.key_type),
2272 );
2273 for f in &a.store_fields {
2274 out.push_str(&format!(
2275 "\tstore {}: {}\n",
2276 f.name.name,
2277 store_kind_str(&f.kind)
2278 ));
2279 }
2280 for inv in &a.invariants {
2281 out.push_str(&format!(
2282 "\tinvariant {}: {}\n",
2283 inv.name.name,
2284 bynk_fmt::expr_to_string(&inv.predicate)
2285 ));
2286 }
2287 for tr in &a.transitions {
2288 out.push_str(&format!(
2289 "\ttransition {}: {}\n",
2290 tr.name.name,
2291 bynk_fmt::expr_to_string(&tr.predicate)
2292 ));
2293 }
2294 out.push_str("}\n```\n");
2295 if let Some(doc) = &a.documentation {
2296 out.push('\n');
2297 out.push_str(doc);
2298 out.push('\n');
2299 }
2300 out
2301}
2302
2303pub(crate) fn describe_provider(p: &ProviderDecl) -> String {
2304 let mut out = format!(
2305 "```bynk\nprovides {} = {}\n```\n",
2306 p.capability.name, p.provider_name.name
2307 );
2308 if let Some(doc) = &p.documentation {
2309 out.push('\n');
2310 out.push_str(doc);
2311 out.push('\n');
2312 }
2313 out
2314}
2315
2316pub struct CrossFileSymbol {
2321 pub path: PathBuf,
2322 pub span: Span,
2323 pub source: String,
2324}
2325
2326fn sorted_paths(files: &HashMap<PathBuf, String>) -> Vec<&PathBuf> {
2331 let mut paths: Vec<&PathBuf> = files.keys().collect();
2332 paths.sort();
2333 paths
2334}
2335
2336pub fn find_declaration_cross_file(
2347 files: &HashMap<PathBuf, String>,
2348 current_path: &Path,
2349 name: &str,
2350) -> Option<CrossFileSymbol> {
2351 for path in sorted_paths(files) {
2352 if path.as_path() == current_path {
2353 continue;
2354 }
2355 let source = &files[path];
2356 if let Some(span) = find_declaration_span(source, name) {
2357 return Some(CrossFileSymbol {
2358 path: path.clone(),
2359 span,
2360 source: source.clone(),
2361 });
2362 }
2363 }
2364 None
2365}
2366
2367pub fn describe_symbol_cross_file(
2373 files: &HashMap<PathBuf, String>,
2374 current_path: &Path,
2375 name: &str,
2376) -> Option<(PathBuf, String)> {
2377 for path in sorted_paths(files) {
2378 if path.as_path() == current_path {
2379 continue;
2380 }
2381 let source = &files[path];
2382 if let Some(desc) = describe_symbol(source, name) {
2383 return Some((path.clone(), desc));
2384 }
2385 }
2386 None
2387}
2388
2389pub fn type_ref_str(t: &TypeRef) -> String {
2390 match t {
2391 TypeRef::Fn(params, ret, _) => {
2393 let lhs = match params.len() {
2394 0 => "()".to_string(),
2395 1 if !matches!(params[0], TypeRef::Fn(..)) => type_ref_str(¶ms[0]),
2396 _ => format!(
2397 "({})",
2398 params
2399 .iter()
2400 .map(type_ref_str)
2401 .collect::<Vec<_>>()
2402 .join(", ")
2403 ),
2404 };
2405 format!("{lhs} -> {}", type_ref_str(ret))
2406 }
2407 TypeRef::Base(b, _) => b.name().to_string(),
2408 TypeRef::Named(id) => id.name.clone(),
2409 TypeRef::Result(a, b, _) => format!("Result[{}, {}]", type_ref_str(a), type_ref_str(b)),
2410 TypeRef::Option(t, _) => format!("Option[{}]", type_ref_str(t)),
2411 TypeRef::Effect(t, _) => format!("Effect[{}]", type_ref_str(t)),
2412 TypeRef::HttpResult(t, _) => format!("HttpResult[{}]", type_ref_str(t)),
2413 TypeRef::QueueResult(_) => "QueueResult".to_string(),
2414 TypeRef::Asset(_) => "Asset".to_string(),
2415 TypeRef::RequestHeaders(_) => "RequestHeaders".to_string(),
2416 TypeRef::TokenClaims(_) => "TokenClaims".to_string(),
2417 TypeRef::BoundaryRejection(_) => "BoundaryRejection".to_string(),
2418 TypeRef::List(t, _) => format!("List[{}]", type_ref_str(t)),
2420 TypeRef::Query(t, _) => format!("Query[{}]", type_ref_str(t)),
2421 TypeRef::Stream(t, _) => format!("Stream[{}]", type_ref_str(t)),
2422 TypeRef::Connection(t, _) => format!("Connection[{}]", type_ref_str(t)),
2423 TypeRef::History(t, _) => format!("History[{}]", type_ref_str(t)),
2424 TypeRef::Map(k, v, _) => format!("Map[{}, {}]", type_ref_str(k), type_ref_str(v)),
2425 TypeRef::ValidationError(_) => "ValidationError".to_string(),
2426 TypeRef::JsonError(_) => "JsonError".to_string(),
2427 TypeRef::Unit(_) => "()".to_string(),
2428 TypeRef::App { name, args, .. } => format!(
2430 "{}[{}]",
2431 name.name,
2432 args.iter().map(type_ref_str).collect::<Vec<_>>().join(", ")
2433 ),
2434 }
2435}
2436
2437#[cfg(test)]
2438mod tests {
2439 use super::*;
2440
2441 #[test]
2445 fn system_job_hover_needs_a_job_service_receiver() {
2446 const CTX: &str = "context cr.jobs\n\ntype Envelope = { message: String }\n\nservice sweeper from cron {\n on schedule(\"*/5 * * * *\") () -> Effect[Result[(), String]] {\n Ok(())\n }\n}\n\nservice deliveries from queue(\"d\") {\n on message(e: Envelope) -> Effect[QueueResult] {\n Ack\n }\n}\n";
2447 let suite = |tier: &str| {
2448 format!(
2449 "suite cr.jobs{tier} {{\n case \"c\" {{\n let e = Envelope {{ message: \"hi\" }}\n let m = e.message\n let r <- deliveries.message(e)\n let s <- sweeper.schedule(\"*/5 * * * *\")\n expect m == \"hi\"\n }}\n}}\n"
2450 )
2451 };
2452 let at = |src: &str, needle: &str| {
2453 src.find(needle).expect("needle is in the suite") + needle.len() - "message".len()
2454 };
2455 let system = suite(" as system");
2456 let field = at(&system, "e.message");
2457 assert!(describe_system_job_address_at(&system, field, [CTX]).is_none());
2458 let queue = at(&system, "deliveries.message");
2459 let hover =
2460 describe_system_job_address_at(&system, queue, [CTX]).expect("queue address hovers");
2461 assert!(hover.contains("`queue` entry"), "{hover}");
2462 let cron = system.find("sweeper.schedule").expect("cron address") + "sweeper.".len();
2463 let hover =
2464 describe_system_job_address_at(&system, cron, [CTX]).expect("cron address hovers");
2465 assert!(hover.contains("`scheduled` entry"), "{hover}");
2466 let unit = suite("");
2467 let queue = at(&unit, "deliveries.message");
2468 assert!(describe_system_job_address_at(&unit, queue, [CTX]).is_none());
2469 }
2470
2471 #[test]
2474 fn run_id_hovers_only_as_the_test_run() {
2475 let src = "suite rid.orders {\n case \"a\" {\n expect run.id == \"local\"\n }\n case \"b\" {\n let run = Wrapper { id: \"x\" }\n expect run.id == \"x\"\n }\n}\n";
2476 let first = src.find("run.id").expect("first run.id");
2477 assert!(describe_run_id_at(src, first).is_some());
2478 assert!(describe_run_id_at(src, first + "run.".len()).is_some());
2479 let shadowed = src.rfind("run.id").expect("second run.id");
2480 assert!(describe_run_id_at(src, shadowed).is_none());
2481 let interp = "suite rid.orders as system {\n case \"a\" {\n let r <- api.POST(\"/o/:id\", \"o-\\(run.id)\") by Visitor\n expect r is Ok(true)\n }\n}\n";
2482 assert!(describe_run_id_at(interp, interp.find("run.id").expect("run.id")).is_some());
2483 let ctx = "context rid.orders\n\nfn f(run: Wrapper) -> String {\n run.id\n}\n";
2484 assert!(describe_run_id_at(ctx, ctx.find("run.id").expect("run.id")).is_none());
2485 }
2486
2487 #[test]
2492 fn run_id_hover_respects_nested_binders() {
2493 let src = "suite rid.orders {\n case \"a\" {\n let w = Wrapper { id: \"x\" }\n let a = if true {\n let run = w\n run.id\n } else { \"\" }\n let f = (run: Wrapper) => run.id\n let m = match Some(w) { Some(run) => run.id, None => \"\" }\n let i = if Some(w) is Some(run) { run.id } else { \"\" }\n let j = Some(w) is Some(run) && run.id == \"x\"\n let k = if !(Some(w) is Some(run)) { \"\" } else { run.id }\n expect run.id == \"local\"\n }\n}\n";
2494 let starts: Vec<usize> = src.match_indices("run.id").map(|(i, _)| i).collect();
2495 assert_eq!(starts.len(), 7, "{src}");
2496 for &shadowed in &starts[..6] {
2497 assert!(
2498 describe_run_id_at(src, shadowed).is_none(),
2499 "shadowed at {shadowed}: {}",
2500 &src[shadowed.saturating_sub(30)..shadowed + 6]
2501 );
2502 }
2503 assert!(describe_run_id_at(src, starts[6]).is_some(), "the test run");
2504 }
2505
2506 #[test]
2509 fn hover_describes_reserved_principals() {
2510 let src = "suite pay.hooks as system {\n\tcase \"c\" {\n\t\tlet r <- webhooks.POST(\"/h\", \"x\") by Forged\n\t\tlet Stale = 1\n\t\texpect r is Rejected(Unauthorized)\n\t}\n}\n";
2511 let forged = describe_reserved_principal_at(src, src.find("Forged").unwrap()).unwrap();
2512 assert!(
2513 forged.contains("by Forged") && forged.contains("does not hold"),
2514 "{forged}"
2515 );
2516 assert!(describe_reserved_principal_at(src, src.find("Stale").unwrap()).is_none());
2517 }
2518
2519 #[test]
2522 fn hover_describes_env_entries() {
2523 let src = "suite shop.pay {\n\tenv {\n\t\tA = \"x\"\n\t}\n\n\tcase \"c\" {\n\t\tenv { A = \"y\"\n\t\t\tB = \"z\" }\n\t\texpect true\n\t}\n}\n";
2524 let at = |needle: &str| src.find(needle).unwrap();
2525 assert_eq!(
2526 describe_env_at(src, at("env")).as_deref(),
2527 Some(ENV_KEYWORD_DOC)
2528 );
2529 let suite_a = describe_env_at(src, at("A = \"x\"")).unwrap();
2530 assert!(suite_a.contains("A = \"x\""), "{suite_a}");
2531 assert!(suite_a.ends_with("(suite)"), "{suite_a}");
2532 let case_a = describe_env_at(src, at("A = \"y\"")).unwrap();
2533 assert!(case_a.ends_with("(case, overrides suite)"), "{case_a}");
2534 let case_b = describe_env_at(src, at("B = ")).unwrap();
2535 assert!(case_b.ends_with("(case)"), "{case_b}");
2536 assert!(describe_env_at(src, at("expect")).is_none());
2537 }
2538
2539 #[test]
2542 fn hover_marks_an_extensible_first_party_enum() {
2543 let fetch_error = describe_firstparty_symbol("FetchError").expect("FetchError hovers");
2544 assert!(fetch_error.contains("**Extensible**"), "{fetch_error}");
2545 assert!(
2546 !fetch_error.contains("**Experimental**"),
2547 "nothing ships experimental"
2548 );
2549 assert!(
2550 describe_firstparty_symbol("Method")
2551 .unwrap()
2552 .contains("**Extensible**")
2553 );
2554 let closed = describe_firstparty_symbol("Request").expect("Request hovers");
2555 assert!(
2556 !closed.contains("**Extensible**"),
2557 "a closed first-party type is not marked extensible"
2558 );
2559 assert_eq!(
2560 firstparty_notes("app.user", "FetchError"),
2561 "",
2562 "a user's own enum"
2563 );
2564 }
2565
2566 fn project_files(root: &Path, files: &[(&str, &str)]) -> HashMap<PathBuf, String> {
2572 files
2573 .iter()
2574 .map(|(rel, contents)| (root.join(rel), (*contents).to_string()))
2575 .collect()
2576 }
2577
2578 #[test]
2579 fn receiver_segment_survives_a_multibyte_char_before_the_receiver() {
2580 let text = "\"€p.items"; let member_start = text.rfind("items").unwrap();
2585 let (recv, start) =
2586 receiver_segment_at(text, Span::new(member_start, text.len())).expect("receiver");
2587 assert_eq!(recv, "p");
2588 assert_eq!(&text[start..start + recv.len()], "p");
2589 }
2590
2591 #[test]
2592 fn cross_file_definition_resolves_into_sibling_file() {
2593 let root = PathBuf::from("/synthetic/cross_file_definition");
2594 let files = project_files(
2595 &root,
2596 &[
2597 (
2598 "a.bynk",
2599 "commons demo.a\n\ntype Foo = Int where Positive\n",
2600 ),
2601 (
2602 "b.bynk",
2603 "commons demo.b\n\nuses demo.a\n\ntype Bar = Int where NonNegative\n",
2604 ),
2605 ],
2606 );
2607 let current = root.join("b.bynk");
2608 let found = find_declaration_cross_file(&files, ¤t, "Foo")
2609 .expect("Foo should resolve into a.bynk");
2610 let expected = root.join("a.bynk");
2611 assert_eq!(found.path, expected);
2612 assert!(
2613 found.source.contains("type Foo = Int where Positive"),
2614 "source returned does not contain Foo declaration"
2615 );
2616 }
2617
2618 #[test]
2619 fn cross_file_definition_skips_current_file() {
2620 let root = PathBuf::from("/synthetic/cross_file_skip_current");
2621 let files = project_files(
2622 &root,
2623 &[(
2624 "only.bynk",
2625 "commons demo.only\n\ntype Foo = Int where Positive\n",
2626 )],
2627 );
2628 let current = root.join("only.bynk");
2629 assert!(find_declaration_cross_file(&files, ¤t, "Foo").is_none());
2631 }
2632
2633 #[test]
2634 fn cross_file_hover_returns_markdown_summary() {
2635 let root = PathBuf::from("/synthetic/cross_file_hover");
2636 let files = project_files(
2637 &root,
2638 &[
2639 (
2640 "money.bynk",
2641 "commons demo.money\n\n\
2642 ---\n\
2643 Amount in minor units of currency.\n\
2644 ---\n\
2645 type Money = Int where NonNegative\n",
2646 ),
2647 (
2648 "orders.bynk",
2649 "commons demo.orders\n\nuses demo.money\n\ntype OrderId = Int where Positive\n",
2650 ),
2651 ],
2652 );
2653 let current = root.join("orders.bynk");
2654 let (other_path, desc) = describe_symbol_cross_file(&files, ¤t, "Money")
2655 .expect("Money should produce hover content");
2656 assert_eq!(other_path, root.join("money.bynk"));
2657 assert!(desc.contains("type Money"));
2658 assert!(
2659 desc.contains("Amount in minor units"),
2660 "hover should include the doc block"
2661 );
2662 }
2663
2664 #[test]
2665 fn cross_file_returns_none_for_unknown_name() {
2666 let root = PathBuf::from("/synthetic/cross_file_none");
2667 let files = project_files(
2668 &root,
2669 &[(
2670 "a.bynk",
2671 "commons demo.a\n\ntype Foo = Int where Positive\n",
2672 )],
2673 );
2674 let current = root.join("a.bynk");
2675 assert!(find_declaration_cross_file(&files, ¤t, "DoesNotExist").is_none());
2676 assert!(describe_symbol_cross_file(&files, ¤t, "DoesNotExist").is_none());
2677 }
2678
2679 #[test]
2680 fn first_party_symbols_describe_their_signature_and_doc() {
2681 let reverse = describe_firstparty_symbol("reverse").expect("`bynk.list.reverse` described");
2685 assert!(
2686 reverse.contains("reverse") && reverse.contains("List"),
2687 "{reverse}"
2688 );
2689 assert!(
2690 reverse.contains("reverse order"),
2691 "doc block surfaced: {reverse}"
2692 );
2693 let clock = describe_firstparty_symbol("Clock").expect("`bynk`-surface `Clock`");
2695 assert!(
2696 clock.contains("wall-clock"),
2697 "capability doc surfaced: {clock}"
2698 );
2699 let locale_tag = describe_firstparty_symbol("LocaleTag")
2703 .expect("`bynk.locale.types.LocaleTag` described");
2704 assert!(
2705 locale_tag.contains("LocaleTag") && locale_tag.contains("Matches"),
2706 "{locale_tag}"
2707 );
2708 let render = describe_firstparty_symbol("render").expect("`bynk.locale.render` described");
2709 assert!(
2710 render.contains("render") && render.contains("LocaleTag"),
2711 "{render}"
2712 );
2713 let with_text =
2714 describe_firstparty_symbol("withText").expect("`bynk.locale.withText` described");
2715 assert!(with_text.contains("withText"), "{with_text}");
2716 assert!(describe_firstparty_symbol("DoesNotExist").is_none());
2718 }
2719
2720 #[test]
2721 fn unit_reference_spans_finds_uses_and_consumes_targets() {
2722 let src = "context app.main\n uses billing.charge\n consumes platform.time\n";
2725 let spans = unit_reference_spans(src);
2726 let names: Vec<&str> = spans.iter().map(|(n, _)| n.as_str()).collect();
2727 assert!(names.contains(&"billing.charge"), "{names:?}");
2728 assert!(names.contains(&"platform.time"), "{names:?}");
2729 let (_, span) = spans.iter().find(|(n, _)| n == "billing.charge").unwrap();
2731 assert_eq!(&src[span.start..span.end], "billing.charge");
2732 }
2733
2734 #[test]
2735 fn unit_reference_spans_links_the_suite_target() {
2736 let src = "suite todos\n uses billing.charge\n";
2739 let spans = unit_reference_spans(src);
2740 let names: Vec<&str> = spans.iter().map(|(n, _)| n.as_str()).collect();
2741 assert!(names.contains(&"todos"), "{names:?}");
2742 assert!(names.contains(&"billing.charge"), "{names:?}");
2743 let (_, span) = spans.iter().find(|(n, _)| n == "todos").unwrap();
2745 assert_eq!(&src[span.start..span.end], "todos");
2746 }
2747
2748 #[test]
2749 fn scan_doc_link_candidates_finds_a_bare_shortcut() {
2750 let cands = scan_doc_link_candidates("See [Limiter] for details.");
2751 assert_eq!(cands.len(), 1);
2752 assert_eq!(cands[0].name, "Limiter");
2753 assert_eq!(cands[0].display, "Limiter");
2754 assert_eq!(
2755 &"See [Limiter] for details."[cands[0].span.clone()],
2756 "[Limiter]"
2757 );
2758 }
2759
2760 #[test]
2761 fn scan_doc_link_candidates_finds_a_dotted_shortcut() {
2762 let cands = scan_doc_link_candidates("See [RateView.remaining].");
2763 assert_eq!(cands.len(), 1);
2764 assert_eq!(cands[0].name, "RateView.remaining");
2765 }
2766
2767 #[test]
2768 fn scan_doc_link_candidates_finds_a_code_span_shortcut() {
2769 let text = "The [`RateView.remaining`] field is never negative.";
2770 let cands = scan_doc_link_candidates(text);
2771 assert_eq!(cands.len(), 1);
2772 assert_eq!(cands[0].name, "RateView.remaining");
2773 assert_eq!(cands[0].display, "`RateView.remaining`");
2774 assert_eq!(&text[cands[0].span.clone()], "[`RateView.remaining`]");
2775 }
2776
2777 #[test]
2778 fn scan_doc_link_candidates_finds_a_full_reference() {
2779 let cands = scan_doc_link_candidates("See [the limiter][Limiter] for details.");
2780 assert_eq!(cands.len(), 1);
2781 assert_eq!(cands[0].name, "Limiter");
2782 assert_eq!(cands[0].display, "the limiter");
2783 }
2784
2785 #[test]
2786 fn scan_doc_link_candidates_leaves_an_explicit_url_untouched() {
2787 let cands = scan_doc_link_candidates("See [Limiter](https://example.com).");
2788 assert!(cands.is_empty(), "{cands:?}");
2789 }
2790
2791 #[test]
2792 fn scan_doc_link_candidates_leaves_an_author_defined_reference_untouched() {
2793 let text = "See [foo] for details.\n\n[foo]: https://example.com\n";
2794 let cands = scan_doc_link_candidates(text);
2795 assert!(cands.is_empty(), "{cands:?}");
2796 }
2797
2798 #[test]
2799 fn scan_doc_link_candidates_ignores_non_identifier_bracket_content() {
2800 let cands = scan_doc_link_candidates("See [see note] and [1] below.");
2801 assert!(cands.is_empty(), "{cands:?}");
2802 }
2803
2804 #[test]
2805 fn scan_doc_link_candidates_ignores_fenced_code_block_content() {
2806 let text = "Example:\n```\n[Bracket, Syntax]\n[label]: not-a-real-def\n```\nSee [Limiter].";
2807 let cands = scan_doc_link_candidates(text);
2808 assert_eq!(cands.len(), 1);
2809 assert_eq!(cands[0].name, "Limiter");
2810 }
2811
2812 #[test]
2813 fn scan_doc_link_candidates_does_not_recognise_the_collapsed_reference_form() {
2814 let cands = scan_doc_link_candidates("See [Limiter][].");
2817 assert!(cands.is_empty(), "{cands:?}");
2818 }
2819
2820 #[test]
2821 fn doc_link_spans_reports_absolute_offsets_in_an_indented_doc_block() {
2822 let src = "context app.main\n ---\n See [Limiter] here.\n ---\n service api {}\n";
2823 let spans = doc_link_spans(src);
2824 assert_eq!(spans.len(), 1);
2825 let (name, span) = &spans[0];
2826 assert_eq!(name, "Limiter");
2827 assert_eq!(&src[span.start..span.end], "[Limiter]");
2828 }
2829
2830 #[test]
2832 fn doc_link_spans_finds_links_in_doc_lines() {
2833 let src = "--- Wraps a [Foo].\n--- And a [Bar].\ntype Baz = Int\n";
2834 let spans = doc_link_spans(src);
2835 let names: Vec<&str> = spans.iter().map(|(n, _)| n.as_str()).collect();
2836 assert_eq!(names, vec!["Foo", "Bar"]);
2837 assert_eq!(&src[spans[1].1.start..spans[1].1.end], "[Bar]");
2838 }
2839
2840 #[test]
2841 fn doc_link_spans_finds_links_across_multiple_doc_blocks() {
2842 let src = "---\nSee [Foo].\n---\ntype Foo = Int\n\n---\nSee [Bar].\n---\ntype Bar = Int\n";
2843 let spans = doc_link_spans(src);
2844 let names: Vec<&str> = spans.iter().map(|(n, _)| n.as_str()).collect();
2845 assert_eq!(names, vec!["Foo", "Bar"]);
2846 }
2847
2848 #[test]
2849 fn own_declaration_name_finds_context_and_commons_names() {
2850 let src = "context app.main\n uses billing.charge\n";
2852 let (name, span) = own_declaration_name(src).expect("context has its own name");
2853 assert_eq!(name, "app.main");
2854 assert_eq!(&src[span.start..span.end], "app.main");
2855
2856 let src = "commons app.util\n\ntype Foo = Int where Positive\n";
2857 let (name, _) = own_declaration_name(src).expect("commons has its own name");
2858 assert_eq!(name, "app.util");
2859 }
2860
2861 #[test]
2862 fn own_declaration_name_none_for_suites() {
2863 let src = "suite todos\n";
2867 assert!(own_declaration_name(src).is_none());
2868 }
2869
2870 #[test]
2873 fn describe_type_renders_fields_variants_and_refinements() {
2874 let record = describe_symbol(
2875 "commons demo.m\n\ntype Order = {\n id: OrderId,\n total: Money,\n}\n",
2876 "Order",
2877 )
2878 .unwrap();
2879 assert!(record.contains("type Order = {"), "{record}");
2880 assert!(record.contains("id: OrderId"), "{record}");
2881 assert!(record.contains("total: Money"), "{record}");
2882
2883 let sum = describe_symbol(
2884 "commons demo.m\n\ntype Status = enum { Pending, Shipped }\n",
2885 "Status",
2886 )
2887 .unwrap();
2888 assert!(sum.contains("enum {"), "{sum}");
2889 assert!(sum.contains("Pending") && sum.contains("Shipped"), "{sum}");
2890
2891 let refined = describe_symbol(
2892 "commons demo.m\n\ntype Email = String where NonEmpty\n",
2893 "Email",
2894 )
2895 .unwrap();
2896 assert!(
2897 refined.contains("type Email = String where NonEmpty"),
2898 "{refined}"
2899 );
2900
2901 let opaque =
2902 describe_symbol("commons demo.m\n\ntype Token = opaque String\n", "Token").unwrap();
2903 assert!(opaque.contains("type Token = opaque String"), "{opaque}");
2904 }
2905
2906 #[test]
2909 fn describe_fn_renders_contracts() {
2910 let src = "commons demo.m\n\nfn discount(p: Int, pct: Int) -> Int\n requires p_nonneg: p >= 0\n ensures never_negative: result >= 0\n{\n p\n}\n";
2911 let out = describe_symbol(src, "discount").unwrap();
2912 assert!(
2913 out.contains("fn discount(p: Int, pct: Int) -> Int"),
2914 "{out}"
2915 );
2916 assert!(out.contains("requires p_nonneg: p >= 0"), "{out}");
2917 assert!(out.contains("ensures never_negative: result >= 0"), "{out}");
2918 }
2919
2920 #[test]
2922 fn describe_service_renders_routes() {
2923 let src = "context demo.app\n\nservice greeter {\n on call(name: String) -> Effect[String] {\n Effect.pure(name)\n }\n}\n";
2924 let out = describe_symbol(src, "greeter").unwrap();
2925 assert!(out.contains("service greeter {"), "{out}");
2926 assert!(out.contains("on call"), "{out}");
2927 }
2928
2929 #[test]
2932 fn describe_agent_renders_store_and_invariants() {
2933 let src = "context demo.app\n\nagent Counter {\n key id: String\n store count: Cell[Int] = 0\n invariant non_negative: count >= 0\n transition monotonic: new.count >= old.count\n on call bump() -> Effect[Result[(), String]] {\n Ok(())\n }\n}\n";
2934 let out = describe_symbol(src, "Counter").unwrap();
2935 assert!(out.contains("agent Counter {"), "{out}");
2936 assert!(out.contains("key id: String"), "{out}");
2937 assert!(out.contains("store count: Cell[Int]"), "{out}");
2938 assert!(out.contains("invariant non_negative: count >= 0"), "{out}");
2939 assert!(
2940 out.contains("transition monotonic: new.count >= old.count"),
2941 "{out}"
2942 );
2943 }
2944
2945 #[test]
2948 fn qualified_callee_detects_upper_receiver_only() {
2949 let text = " let t = Clock.now()";
2950 let now = text.find("now").unwrap();
2951 assert_eq!(
2952 qualified_callee_at(text, Span::new(now, now + 3)).as_deref(),
2953 Some("Clock.now")
2954 );
2955 let text2 = " xs.fold(0)";
2958 let fold = text2.find("fold").unwrap();
2959 assert!(qualified_callee_at(text2, Span::new(fold, fold + 4)).is_none());
2960 let text3 = " total";
2962 assert!(qualified_callee_at(text3, Span::new(2, 7)).is_none());
2963 }
2964
2965 #[test]
2969 fn describe_agent_state_covers_key_store_keywords_and_fields() {
2970 let src = "context demo.app\n\nagent Sessions {\n key id: String\n store items: Map[String, Int] @indexed( by: id ) @bounded( 10000 )\n on call read() -> Effect[Int] {\n Effect.pure(0)\n }\n}\n";
2971
2972 let at_key_kw = src.find("key id").unwrap();
2975 let key_kw = describe_agent_state_at(src, at_key_kw).expect("hover on `key`");
2976 assert!(key_kw.contains("key id: String"), "{key_kw}");
2977 assert!(key_kw.contains("identity field"), "doc line: {key_kw}");
2978 let at_key_name = src.find("id: String").unwrap();
2979 let key_name = describe_agent_state_at(src, at_key_name).expect("hover on the key field");
2980 assert_eq!(key_kw, key_name, "keyword and name hover match");
2981
2982 let at_store_kw = src.find("store items").unwrap();
2985 let store_kw = describe_agent_state_at(src, at_store_kw).expect("hover on `store`");
2986 assert!(
2987 store_kw.contains("store items: Map[String, Int]"),
2988 "{store_kw}"
2989 );
2990 assert!(
2991 store_kw.contains("@indexed(by: id)"),
2992 "annotation: {store_kw}"
2993 );
2994 assert!(
2995 store_kw.contains("@bounded(10000)"),
2996 "annotation: {store_kw}"
2997 );
2998 assert!(
2999 store_kw.contains("persisted agent-state"),
3000 "doc line: {store_kw}"
3001 );
3002 let at_store_name = src.find("items:").unwrap();
3003 let store_name =
3004 describe_agent_state_at(src, at_store_name).expect("hover on the store field");
3005 assert_eq!(store_kw, store_name, "keyword and name hover match");
3006
3007 assert!(describe_agent_state_at(src, src.find("Sessions").unwrap()).is_none());
3010 assert!(describe_agent_state_at(src, src.find("Map").unwrap()).is_none());
3011 assert!(describe_agent_state_at(src, src.find("by: id").unwrap() + 4).is_none());
3014 }
3015
3016 const TODOS: &str = "context demo.todos\n\
3021 \n\
3022 agent Todos {\n\
3023 \x20 key owner: String\n\
3024 \n\
3025 \x20 store items: Map[String, Int]\n\
3026 \x20 store lastSeq: Cell[Int]\n\
3027 \n\
3028 \x20 invariant nonneg: lastSeq >= 0\n\
3029 \n\
3030 \x20 on call add(n: Int) -> Effect[()] {\n\
3031 \x20 let next = lastSeq + 1\n\
3032 \x20 let _ <- items.put(owner, next)\n\
3033 \x20 lastSeq := next\n\
3034 \x20 Effect.pure(())\n\
3035 \x20 }\n\
3036 }\n";
3037
3038 #[test]
3039 fn describe_agent_state_covers_references_in_handler_bodies() {
3040 let src = TODOS;
3041 let at = |needle: &str| src.find(needle).expect("needle is in the fixture");
3042
3043 let store_decl = describe_agent_state_at(src, at("store lastSeq")).unwrap();
3045 assert!(
3046 store_decl.contains("store lastSeq: Cell[Int]"),
3047 "{store_decl}"
3048 );
3049 for (what, needle) in [
3050 ("a bare read", "lastSeq + 1"),
3051 ("a `:=` write target", "lastSeq := next"),
3052 ("an invariant subject", "lastSeq >= 0"),
3053 ] {
3054 let hover = describe_agent_state_at(src, at(needle))
3055 .unwrap_or_else(|| panic!("no hover on {what}"));
3056 assert_eq!(hover, store_decl, "{what} hovers as its declaration");
3057 }
3058 let recv = describe_agent_state_at(src, at("items.put")).expect("hover on the receiver");
3060 assert_eq!(
3061 recv,
3062 describe_agent_state_at(src, at("store items")).unwrap()
3063 );
3064
3065 let key_decl = describe_agent_state_at(src, at("key owner")).unwrap();
3067 let key_ref = describe_agent_state_at(src, at("owner, next")).expect("hover on `owner`");
3068 assert_eq!(key_ref, key_decl);
3069 assert!(key_ref.contains("key owner: String"), "{key_ref}");
3070
3071 assert!(describe_agent_state_at(src, at("next = lastSeq")).is_none());
3074 assert!(describe_agent_state_at(src, at("Effect.pure")).is_none());
3075 }
3076
3077 #[test]
3080 fn describe_store_op_renders_the_operation_signature() {
3081 let src = TODOS;
3082 let at_put = src
3083 .find("put(owner")
3084 .expect("the store op is in the fixture");
3085 let put = describe_store_op_at(src, at_put, &[]).expect("hover on `items.put`");
3086 assert!(put.contains("put(key: K, value: V) -> Effect[()]"), "{put}");
3087 assert!(put.contains("store items: Map[String, Int]"), "{put}");
3089
3090 let shadow = [bynk_check::locals::LocalBinding {
3093 name: "items".into(),
3094 def_span: Span::new(0, 5),
3095 kind: bynk_check::locals::LocalKind::Let,
3096 ty: "Map[String, Int]".into(),
3097 scope: Span::new(0, src.len()),
3098 }];
3099 assert!(describe_store_op_at(src, at_put, &shadow).is_none());
3100
3101 let cell_put = src.replace("items.put(owner, next)", "lastSeq.put(owner, next)");
3104 assert!(
3105 describe_store_op_at(&cell_put, cell_put.find("put(owner").unwrap(), &[]).is_none(),
3106 "a `Cell` has no `put`"
3107 );
3108 assert!(describe_store_op_at(src, src.find("pure(()").unwrap(), &[]).is_none());
3109 assert!(describe_store_op_at(src, src.find("next = lastSeq").unwrap(), &[]).is_none());
3110 }
3111
3112 #[test]
3118 fn a_qualified_receiver_is_not_a_store_op_or_a_state_reference() {
3119 let src = "context demo.todos\n\
3120 \n\
3121 type Inner = { put: Int }\n\
3122 type Payload = { items: Inner, lastSeq: Int }\n\
3123 \n\
3124 agent Todos {\n\
3125 \x20 key owner: String\n\
3126 \x20 store items: Map[String, Int]\n\
3127 \x20 store lastSeq: Cell[Int]\n\
3128 \n\
3129 \x20 on call add(p: Payload) -> Effect[()] {\n\
3130 \x20 let a = p.items.put\n\
3131 \x20 let b = p.lastSeq\n\
3132 \x20 Effect.pure(())\n\
3133 \x20 }\n\
3134 }\n";
3135 let at_put = src.find("put\n").expect("the qualified member");
3137 assert!(describe_store_op_at(src, at_put, &[]).is_none());
3138 let at_items = src.find("p.items").unwrap() + "p.".len();
3141 assert!(describe_agent_state_at(src, at_items).is_none());
3142 let at_seq = src.find("p.lastSeq").unwrap() + "p.".len();
3143 assert!(describe_agent_state_at(src, at_seq).is_none());
3144
3145 let bare = src.find("store items").unwrap();
3147 assert!(describe_agent_state_at(src, bare).is_some());
3148 }
3149
3150 #[test]
3155 fn describe_symbol_renders_a_resolved_record_field() {
3156 let src = "context demo.todos\n\n\
3157 type Title = String where NonEmpty\n\n\
3158 type Stored = {\n\
3159 \x20 seq: Int where NonNegative,\n\
3160 \x20 title: Title,\n\
3161 }\n";
3162 let title = describe_symbol(src, "Stored.title").expect("hover on `Stored.title`");
3163 assert!(title.contains("title: Title"), "{title}");
3164 assert!(title.contains("A field of `Stored`"), "{title}");
3165 let seq = describe_symbol(src, "Stored.seq").expect("hover on `Stored.seq`");
3167 assert!(seq.contains("seq: Int where NonNegative"), "{seq}");
3168
3169 assert!(
3171 describe_symbol(src, "Stored")
3172 .unwrap()
3173 .contains("type Stored")
3174 );
3175 assert!(describe_symbol(src, "Stored.nope").is_none());
3177 assert!(describe_symbol(src, "Nope.title").is_none());
3178 assert!(describe_symbol(src, "Title.title").is_none());
3179 }
3180
3181 #[test]
3186 fn describe_symbol_renders_an_event_and_its_defaulted_field() {
3187 let src = "context demo.orders\n\n\
3188 type Region = enum { Domestic, International }\n\n\
3189 event PaymentConfirmed = {\n\
3190 \x20 orderId: String,\n\
3191 \x20 region: Region = Region.Domestic,\n\
3192 }\n";
3193 let whole = describe_symbol(src, "PaymentConfirmed").expect("hover on the event itself");
3194 assert!(whole.contains("type PaymentConfirmed = {"), "{whole}");
3195 assert!(whole.contains("orderId: String"), "{whole}");
3196 assert!(
3197 whole.contains("region: Region = Region.Domestic"),
3198 "{whole}"
3199 );
3200
3201 let field = describe_symbol(src, "PaymentConfirmed.region")
3202 .expect("hover on `PaymentConfirmed.region`");
3203 assert!(
3204 field.contains("region: Region = Region.Domestic"),
3205 "{field}"
3206 );
3207 assert!(field.contains("A field of `PaymentConfirmed`"), "{field}");
3208
3209 let order_id = describe_symbol(src, "PaymentConfirmed.orderId")
3211 .expect("hover on `PaymentConfirmed.orderId`");
3212 assert!(order_id.contains("orderId: String"), "{order_id}");
3213 assert!(!order_id.contains('='), "{order_id}");
3214
3215 assert!(describe_symbol(src, "PaymentConfirmed.nope").is_none());
3216 assert!(describe_symbol(src, "Nope.region").is_none());
3217 }
3218
3219 #[test]
3223 fn describe_symbol_renders_an_events_schema_annotation() {
3224 let src = "context demo.orders\n\n\
3225 event PaymentConfirmed @schema(2) = {\n\
3226 \x20 orderId: String,\n\
3227 }\n";
3228 let whole = describe_symbol(src, "PaymentConfirmed").expect("hover on the event itself");
3229 assert!(
3230 whole.contains("type PaymentConfirmed @schema(2) = {"),
3231 "{whole}"
3232 );
3233 assert!(whole.contains("orderId: String"), "{whole}");
3234 }
3235
3236 #[test]
3241 fn describe_symbol_renders_an_actor_in_both_forms() {
3242 let src = "context demo.auth\n\n\
3243 type UserId = String where NonEmpty\n\n\
3244 ---\n\
3245 A signed-in user.\n\
3246 ---\n\
3247 actor User { auth = Bearer(secret = \"AUTH_JWT_SECRET\"), identity = UserId }\n\n\
3248 actor Public { auth = None }\n\n\
3249 actor Worker { auth = Internal }\n\n\
3250 actor Admin = User where hasClaim(\"admin\")\n";
3251
3252 let user = describe_symbol(src, "User").expect("hover on `User`");
3253 assert!(
3254 user.contains(
3255 "actor User { auth = Bearer(secret = \"AUTH_JWT_SECRET\"), identity = UserId }"
3256 ),
3257 "{user}"
3258 );
3259 assert!(user.contains("A signed-in user."), "{user}");
3261
3262 let public = describe_symbol(src, "Public").expect("hover on `Public`");
3264 assert!(
3265 public.contains("actor Public { auth = None }") && !public.contains("identity"),
3266 "{public}"
3267 );
3268 assert!(
3269 describe_symbol(src, "Worker")
3270 .unwrap()
3271 .contains("actor Worker { auth = Internal }")
3272 );
3273
3274 let admin = describe_symbol(src, "Admin").expect("hover on `Admin`");
3276 assert!(
3277 admin.contains("actor Admin = User where hasClaim(\"admin\")"),
3278 "{admin}"
3279 );
3280
3281 assert!(describe_symbol(src, "Nobody").is_none());
3282 }
3283
3284 #[test]
3291 fn describe_symbol_escapes_an_actors_scheme_config() {
3292 let src = "context demo.auth\n\n\
3293 actor User { auth = Bearer(secret = \"a\\\"b\\\\c\") }\n";
3294 let hover = describe_symbol(src, "User").expect("hover on `User`");
3295 assert!(
3296 hover.contains("actor User { auth = Bearer(secret = \"a\\\"b\\\\c\") }"),
3297 "the config value must round-trip escaped:\n{hover}"
3298 );
3299
3300 let formatted = bynk_fmt::format_source(src, &bynk_fmt::FormatOptions::default())
3302 .expect("the fixture formats");
3303 let actor_line = formatted
3304 .lines()
3305 .find(|l| l.starts_with("actor User"))
3306 .expect("the actor line");
3307 assert!(
3308 hover.contains(actor_line),
3309 "hover:\n{hover}\nfmt: {actor_line}"
3310 );
3311 }
3312
3313 #[test]
3316 fn describe_symbol_renders_a_capability_operation() {
3317 let src = "context demo.svc\n\n\
3318 capability Logger {\n\
3319 \x20 ---\n\
3320 \x20 Record a line.\n\
3321 \x20 ---\n\
3322 \x20 fn info(message: String) -> Effect[()]\n\
3323 }\n\n\
3324 capability Clock {\n\
3325 \x20 fn now() -> Effect[Int]\n\
3326 }\n";
3327
3328 let info = describe_symbol(src, "Logger.info").expect("hover on `Logger.info`");
3329 assert!(
3330 info.contains("fn info(message: String) -> Effect[()]"),
3331 "{info}"
3332 );
3333 assert!(
3334 info.contains("An operation of capability `Logger`"),
3335 "{info}"
3336 );
3337 assert!(info.contains("Record a line."), "{info}");
3338
3339 let now = describe_symbol(src, "Clock.now").expect("hover on `Clock.now`");
3341 assert!(now.contains("fn now() -> Effect[Int]"), "{now}");
3342
3343 assert!(
3345 describe_symbol(src, "Logger")
3346 .unwrap()
3347 .contains("capability Logger")
3348 );
3349 assert!(describe_symbol(src, "Logger.nope").is_none());
3351 assert!(describe_symbol(src, "Clock.info").is_none());
3352 }
3353
3354 #[test]
3355 fn describe_symbol_renders_a_messages_bundle() {
3356 let src = "commons app.bundle\n\n\
3360 ---\n\
3361 The English reference bundle.\n\
3362 ---\n\
3363 messages \"en\" @reference {\n\
3364 \x20 \"greeting\" => \"Hello, {name}!\"\n\
3365 \x20 \"farewell\" => \"Bye\"\n\
3366 }\n";
3367
3368 let info = describe_symbol(src, "en").expect("hover on the `en` messages tag");
3369 assert!(info.contains("messages \"en\" @reference {"), "{info}");
3370 assert!(info.contains("\"greeting\" => …"), "{info}");
3371 assert!(info.contains("\"farewell\" => …"), "{info}");
3372 assert!(info.contains("The English reference bundle."), "{info}");
3373 }
3374
3375 #[test]
3380 fn describe_symbol_keys_methods_by_their_compound_name_only() {
3381 let src = "context demo.shop\n\n\
3382 type Counter = { count: Int }\n\
3383 type Gauge = { level: Int }\n\n\
3384 fn Counter.bump(self) -> Counter { Counter { count: self.count + 1 } }\n\n\
3385 fn Gauge.bump(self) -> Gauge { Gauge { level: self.level + 1 } }\n\n\
3386 fn free(n: Int) -> Int { n }\n";
3387
3388 let counter = describe_symbol(src, "Counter.bump").expect("hover on `Counter.bump`");
3390 assert!(
3391 counter.contains("fn Counter.bump(self) -> Counter") && !counter.contains("Gauge"),
3392 "{counter}"
3393 );
3394 let gauge = describe_symbol(src, "Gauge.bump").expect("hover on `Gauge.bump`");
3395 assert!(
3396 gauge.contains("fn Gauge.bump(self) -> Gauge") && !gauge.contains("Counter"),
3397 "{gauge}"
3398 );
3399
3400 assert!(describe_symbol(src, "bump").is_none());
3403 assert!(
3405 describe_symbol(src, "free")
3406 .unwrap()
3407 .contains("fn free(n: Int) -> Int")
3408 );
3409 assert!(describe_symbol(src, "Counter.nope").is_none());
3410 }
3411
3412 #[test]
3415 fn describe_self_renders_receiver_and_unwraps_agent() {
3416 use bynk_check::checker::{NamedKind, Ty, Types};
3417 let text = "self";
3418 let tys = &Types::new();
3419 let span = Span::new(0, 4);
3420 let account = vec![(
3422 span,
3423 tys.intern(Ty::Named {
3424 name: "Account".into(),
3425 kind: NamedKind::Record,
3426 args: Vec::new(),
3427 }),
3428 )];
3429 assert_eq!(
3430 describe_self_at(text, 0, &account, tys).as_deref(),
3431 Some("```bynk\nself: Account\n```")
3432 );
3433 let agent = vec![(
3436 span,
3437 tys.intern(Ty::Named {
3438 name: "__CounterSelf".into(),
3439 kind: NamedKind::Record,
3440 args: Vec::new(),
3441 }),
3442 )];
3443 assert_eq!(
3444 describe_self_at(text, 0, &agent, tys).as_deref(),
3445 Some("```bynk\nself: Counter\n```")
3446 );
3447 let other = "total";
3450 assert!(
3451 describe_self_at(
3452 other,
3453 0,
3454 &[(
3455 Span::new(0, 5),
3456 tys.intern(Ty::Named {
3457 name: "Int".into(),
3458 kind: NamedKind::Record,
3459 args: Vec::new(),
3460 }),
3461 )],
3462 tys
3463 )
3464 .is_none()
3465 );
3466 }
3467
3468 const CACHE_SVC: &str = "context api\nservice api from http {\n @cache(maxAge: 5.minutes, scope: public)\n on GET(\"/x\") () -> Effect[HttpResult[String]] by v: Visitor {\n Ok(\"y\")\n }\n}\n";
3469
3470 #[test]
3471 fn an_edge_cache_hovers_with_its_stanza() {
3472 let src =
3474 "context a\nconsumes bynk.cloudflare { EdgeCache }\ncapability Edge = EdgeCache\n";
3475 let tokens = tokenize(src).unwrap();
3476 let (unit, _) = parse_unit_with_recovery(&tokens, src);
3477 let Some(SourceUnit::Context(ctx)) = unit else {
3478 panic!("a context")
3479 };
3480 let decl = ctx
3481 .items
3482 .iter()
3483 .find_map(|i| match i {
3484 CommonsItem::Capability(c) => Some(c),
3485 _ => None,
3486 })
3487 .unwrap();
3488 let hover = describe_derived_capability(decl, Some("bynk.cloudflare"), None);
3489 assert!(hover.contains("Emits `[cache] enabled = true`"), "{hover}");
3490 assert!(!hover.contains("its own storage"), "{hover}");
3491 }
3492
3493 #[test]
3494 fn a_rate_limiter_hovers_with_its_stanza() {
3495 let src = "context a\nconsumes bynk.cloudflare { RateLimit }\ncapability LoginLimit = RateLimit @limit(20) @period(1.minutes)\n";
3497 let tokens = tokenize(src).unwrap();
3498 let (unit, _) = parse_unit_with_recovery(&tokens, src);
3499 let Some(SourceUnit::Context(ctx)) = unit else {
3500 panic!("a context")
3501 };
3502 let decl = ctx
3503 .items
3504 .iter()
3505 .find_map(|i| match i {
3506 CommonsItem::Capability(c) => Some(c),
3507 _ => None,
3508 })
3509 .unwrap();
3510 let hover = describe_derived_capability(decl, Some("bynk.cloudflare"), None);
3511 assert!(
3512 hover.contains(
3513 "`simple = { limit = 20, period = 60 }`: 20 calls per key every 60 seconds"
3514 ),
3515 "{hover}"
3516 );
3517 assert!(hover.contains("approximate"), "{hover}");
3518 }
3519
3520 #[test]
3521 fn hover_on_cache_annotation_describes_it() {
3522 let offset = CACHE_SVC.find("cache").unwrap() + 1;
3524 let hover = describe_handler_annotation_at(CACHE_SVC, offset).expect("hovers @cache");
3525 assert!(hover.contains("`@cache`"), "names the annotation: {hover}");
3526 assert!(hover.contains("maxAge"), "documents maxAge: {hover}");
3527 assert!(hover.contains("scope"), "documents scope: {hover}");
3528 assert!(hover.contains("immutable"), "documents immutable: {hover}");
3529 assert!(
3530 hover.contains("Emits `Cache-Control: public, max-age=300`."),
3531 "shows the emitted header: {hover}"
3532 );
3533 let ok_offset = CACHE_SVC.find("Ok(").unwrap() + 1;
3535 assert!(describe_handler_annotation_at(CACHE_SVC, ok_offset).is_none());
3536 }
3537
3538 #[test]
3539 fn hover_on_immutable_cache_shows_the_directive() {
3540 let svc = "context api\nservice api from http {\n @cache(maxAge: 365.days, scope: public, immutable: true)\n on GET(\"/x\") () -> Effect[HttpResult[String]] by v: Visitor {\n Ok(\"y\")\n }\n}\n";
3541 let hover = describe_handler_annotation_at(svc, svc.find("cache").unwrap() + 1)
3542 .expect("hovers @cache");
3543 assert!(
3544 hover.contains("Emits `Cache-Control: public, max-age=31536000, immutable`."),
3545 "{hover}"
3546 );
3547 }
3548
3549 #[test]
3550 fn hover_on_raw_annotation_shows_content_type_and_effective_cap() {
3551 let svc = "context api\nservice api from http {\n limits { maxBody: 1000 }\n @raw(contentType: \"image/png\")\n on POST(\"/a\") (body: Bytes) -> Effect[HttpResult[String]] by v: Visitor {\n Ok(\"y\")\n }\n @raw(contentType: \"*/*\")\n @limit(maxBody: 50)\n on POST(\"/b\") (body: Bytes) -> Effect[HttpResult[String]] by v: Visitor {\n Ok(\"y\")\n }\n}\n";
3552 let first =
3553 describe_handler_annotation_at(svc, svc.find("raw").unwrap() + 1).expect("hovers @raw");
3554 assert!(first.contains("`image/png`"), "names the type: {first}");
3555 assert!(first.contains("1000 bytes"), "service cap: {first}");
3556 let second = describe_handler_annotation_at(svc, svc.rfind("raw").unwrap() + 1)
3557 .expect("hovers @raw");
3558 assert!(second.contains("any"), "wildcard: {second}");
3559 assert!(second.contains("50 bytes"), "the route's own cap: {second}");
3560 }
3561
3562 #[test]
3563 fn annotation_token_spans_cover_name_and_labels() {
3564 let spans = handler_annotation_token_spans(CACHE_SVC);
3565 assert_eq!(spans.len(), 3, "{spans:?}");
3567 let texts: Vec<&str> = spans.iter().map(|s| &CACHE_SVC[s.start..s.end]).collect();
3568 assert_eq!(texts, ["@cache", "maxAge", "scope"]);
3569 let plain = "context api\nservice api from http {\n on GET(\"/x\") () -> Effect[HttpResult[String]] by v: Visitor { Ok(\"y\") }\n}\n";
3571 assert!(handler_annotation_token_spans(plain).is_empty());
3572 }
3573}