1use std::{collections::BTreeMap, ops::Deref, sync::LazyLock};
2
3use ecow::eco_format;
4use tinymist_analysis::stats::GLOBAL_STATS;
5use typst::foundations::{IntoValue, Module, Str, Type};
6
7use crate::{StrRef, adt::interner::Interned};
8use crate::{adt::snapshot_map::SnapshotMap, analysis::SharedContext};
9use crate::{
10 docs::{DocString, VarDoc, identify_pat_docs, identify_tidy_module_docs},
11 prelude::*,
12 syntax::{Decl, DefKind},
13 ty::{BuiltinTy, DynTypeBounds, InsTy, PackageId, SigTy, Ty, TypeVar, TypeVarBounds},
14};
15
16use super::DeclExpr;
17
18pub(crate) fn do_compute_docstring(
19 ctx: &Arc<SharedContext>,
20 fid: TypstFileId,
21 docs: String,
22 kind: DefKind,
23) -> Option<DocString> {
24 let _guard = GLOBAL_STATS.stat(Some(fid), "compute_docstring");
25
26 let checker = DocsChecker {
27 fid,
28 ctx,
29 var_bounds: HashMap::new(),
30 globals: HashMap::default(),
31 locals: SnapshotMap::default(),
32 next_id: 0,
33 };
34 use DefKind::*;
35 match kind {
36 Function | Variable => checker.check_pat_docs(docs),
37 Module => checker.check_module_docs(docs),
38 Constant | Struct | Reference => None,
39 }
40}
41
42struct DocsChecker<'a> {
43 fid: TypstFileId,
44 ctx: &'a Arc<SharedContext>,
45 var_bounds: HashMap<DeclExpr, TypeVarBounds>,
47 globals: HashMap<EcoString, Option<Ty>>,
49 locals: SnapshotMap<EcoString, Ty>,
51 next_id: u32,
53}
54
55static EMPTY_MODULE: LazyLock<Module> =
56 LazyLock::new(|| Module::new("stub", typst::foundations::Scope::new()));
57
58impl DocsChecker<'_> {
59 pub fn check_pat_docs(mut self, docs: String) -> Option<DocString> {
60 let docs_text = crate::docs::DocText::plain(docs.as_str().into());
61 let converted = crate::docs::convert_docs(self.ctx, &docs_text, Some(self.fid))
62 .and_then(|converted| identify_pat_docs(&converted));
63
64 let converted = match Self::fallback_docs(converted, &docs) {
65 Ok(docs) => docs,
66 Err(err) => return Some(err),
67 };
68
69 let module = self.ctx.module_by_str(docs);
70 let module = module.as_ref().unwrap_or(EMPTY_MODULE.deref());
71
72 let mut params = BTreeMap::new();
73 for param in converted.params.into_iter() {
74 params.insert(
75 param.name.into(),
76 VarDoc {
77 docs: self.ctx.remove_html(param.docs),
78 ty: self.check_type_strings(module, ¶m.types),
79 },
80 );
81 }
82
83 let res_ty = converted
84 .return_ty
85 .and_then(|ty| self.check_type_strings(module, &ty));
86
87 Some(DocString {
88 docs: Some(self.ctx.remove_html(converted.docs)),
89 var_bounds: self.var_bounds,
90 vars: params,
91 res_ty,
92 })
93 }
94
95 pub fn check_module_docs(self, docs: String) -> Option<DocString> {
96 let docs_text = crate::docs::DocText::plain(docs.as_str().into());
97 let converted = crate::docs::convert_docs(self.ctx, &docs_text, Some(self.fid))
98 .and_then(identify_tidy_module_docs);
99
100 let converted = match Self::fallback_docs(converted, &docs) {
101 Ok(docs) => docs,
102 Err(err) => return Some(err),
103 };
104
105 Some(DocString {
106 docs: Some(self.ctx.remove_html(converted.docs)),
107 var_bounds: self.var_bounds,
108 vars: BTreeMap::new(),
109 res_ty: None,
110 })
111 }
112
113 fn fallback_docs<T>(converted: Result<T, EcoString>, docs: &str) -> Result<T, DocString> {
114 match converted {
115 Ok(converted) => Ok(converted),
116 Err(err) => {
117 let err = err.replace("`", "\\`");
118 let max_consecutive_backticks = docs
119 .chars()
120 .fold((0, 0), |(max, count), ch| {
121 if ch == '`' {
122 (max.max(count + 1), count + 1)
123 } else {
124 (max, 0)
125 }
126 })
127 .0;
128 let backticks = "`".repeat((max_consecutive_backticks + 1).max(3));
129 let fallback_docs = eco_format!(
130 "```\nfailed to parse docs: {err}\n```\n\n{backticks}typ\n{docs}\n{backticks}\n"
131 );
132 Err(DocString {
133 docs: Some(fallback_docs),
134 var_bounds: HashMap::new(),
135 vars: BTreeMap::new(),
136 res_ty: None,
137 })
138 }
139 }
140 }
141
142 fn generate_var(&mut self, name: StrRef) -> Ty {
143 self.next_id += 1;
144 let encoded = Interned::new(Decl::generated(DefId(self.next_id as u64)));
145 crate::log_debug_ct!("generate var {name:?} {encoded:?}");
146 let var = TypeVar {
147 name,
148 def: encoded.clone(),
149 };
150 let bounds = TypeVarBounds::new(var, DynTypeBounds::default());
151 let var = bounds.as_type();
152 self.var_bounds.insert(encoded, bounds);
153 var
154 }
155
156 fn check_type_strings(&mut self, m: &Module, inputs: &str) -> Option<Ty> {
157 let mut terms = vec![];
158 for name in inputs.split(",").map(|ty| ty.trim()) {
159 let Some(ty) = self.check_type_ident(m, name) else {
160 continue;
161 };
162 terms.push(ty);
163 }
164
165 Some(Ty::from_types(terms.into_iter()))
166 }
167
168 fn check_type_ident(&mut self, m: &Module, name: &str) -> Option<Ty> {
169 static TYPE_REPRS: LazyLock<HashMap<&'static str, Ty>> = LazyLock::new(|| {
170 let values = Vec::from_iter(
171 [
172 Value::None,
173 Value::Auto,
174 Value::Int(Default::default()),
176 Value::Float(Default::default()),
177 Value::Length(Default::default()),
178 Value::Angle(Default::default()),
179 Value::Ratio(Default::default()),
180 Value::Relative(Default::default()),
181 Value::Fraction(Default::default()),
182 Value::Str(Default::default()),
183 ]
184 .map(|v| v.ty())
185 .into_iter()
186 .chain([
187 Type::of::<typst::visualize::Color>(),
188 Type::of::<typst::visualize::Gradient>(),
189 Type::of::<typst::visualize::Tiling>(),
190 Type::of::<typst::foundations::Symbol>(),
191 Type::of::<typst::foundations::Version>(),
192 Type::of::<typst::foundations::Bytes>(),
193 Type::of::<typst::foundations::Label>(),
194 Type::of::<typst::foundations::Datetime>(),
195 Type::of::<typst::foundations::Duration>(),
196 Type::of::<typst::foundations::Content>(),
197 Type::of::<typst::foundations::Styles>(),
198 Type::of::<typst::foundations::Array>(),
199 Type::of::<typst::foundations::Dict>(),
200 Type::of::<typst::foundations::Func>(),
201 Type::of::<typst::foundations::Args>(),
202 Type::of::<typst::foundations::Type>(),
203 Type::of::<typst::foundations::Module>(),
204 ]),
205 );
206
207 let shorts = values
208 .clone()
209 .into_iter()
210 .map(|ty| (ty.short_name(), Ty::Builtin(BuiltinTy::Type(ty))));
211 let longs = values
212 .into_iter()
213 .map(|ty| (ty.long_name(), Ty::Builtin(BuiltinTy::Type(ty))));
214 let builtins = [
215 ("any", Ty::Any),
216 ("bool", Ty::Boolean(None)),
217 ("boolean", Ty::Boolean(None)),
218 ("false", Ty::Boolean(Some(false))),
219 ("true", Ty::Boolean(Some(true))),
220 ];
221 HashMap::from_iter(shorts.chain(longs).chain(builtins))
222 });
223
224 let builtin_ty = TYPE_REPRS.get(name).cloned();
225 builtin_ty
226 .or_else(|| self.locals.get(name).cloned())
227 .or_else(|| self.check_type_annotation(m, name))
228 }
229
230 fn check_type_annotation(&mut self, module: &Module, name: &str) -> Option<Ty> {
231 if let Some(term) = self.globals.get(name) {
232 return term.clone();
233 }
234
235 let val = module.scope().get(name)?;
236 crate::log_debug_ct!("check doc type annotation: {name:?}");
237 if let Value::Content(raw) = val.read() {
238 let annotated = raw.clone().unpack::<typst::text::RawElem>().ok()?;
239 let annotated = annotated.text.clone().into_value().cast::<Str>().ok()?;
240 let code = typst::syntax::parse_code(&annotated.as_str().replace('\'', "θ"));
241 let mut exprs = code.cast::<ast::Code>()?.exprs();
242 let term = self.check_type_expr(module, exprs.next()?);
243 self.globals.insert(name.into(), term.clone());
244 term
245 } else {
246 None
247 }
248 }
249
250 fn check_type_expr(&mut self, module: &Module, expr: ast::Expr) -> Option<Ty> {
251 crate::log_debug_ct!("check doc type expr: {expr:?}");
252 match expr {
253 ast::Expr::Ident(ident) => self.check_type_ident(module, ident.get().as_str()),
254 ast::Expr::None(_)
255 | ast::Expr::Auto(_)
256 | ast::Expr::Bool(..)
257 | ast::Expr::Int(..)
258 | ast::Expr::Float(..)
259 | ast::Expr::Numeric(..)
260 | ast::Expr::Str(..) => {
261 SharedContext::const_eval(expr).map(|v| Ty::Value(InsTy::new(v)))
262 }
263 ast::Expr::Binary(binary) => {
264 let mut components = Vec::with_capacity(2);
265 components.push(self.check_type_expr(module, binary.lhs())?);
266
267 let mut rhs = binary.rhs();
268 while let ast::Expr::Binary(binary) = rhs {
269 if binary.op() != ast::BinOp::Or {
270 break;
271 }
272
273 components.push(self.check_type_expr(module, binary.lhs())?);
274 rhs = binary.rhs();
275 }
276
277 components.push(self.check_type_expr(module, rhs)?);
278 Some(Ty::from_types(components.into_iter()))
279 }
280 ast::Expr::FuncCall(call) => match call.callee() {
281 ast::Expr::Ident(callee) => {
282 let name = callee.get().as_str();
283 match name {
284 "array" => Some({
285 let ast::Arg::Pos(pos) = call.args().items().next()? else {
286 return None;
287 };
288
289 Ty::Array(self.check_type_expr(module, pos)?.into())
290 }),
291 "tag" => Some({
292 let ast::Arg::Pos(ast::Expr::Str(s)) = call.args().items().next()?
293 else {
294 return None;
295 };
296 let pkg_id = PackageId::try_from(self.fid).ok();
297 Ty::Builtin(BuiltinTy::Tag(Box::new((
298 s.get().into(),
299 pkg_id.map(From::from),
300 ))))
301 }),
302 _ => None,
303 }
304 }
305 _ => None,
306 },
307 ast::Expr::Closure(closure) => {
308 crate::log_debug_ct!("check doc closure annotation: {closure:?}");
309 let mut pos_all = vec![];
310 let mut named_all = BTreeMap::new();
311 let mut spread_right = None;
312 let snap = self.locals.snapshot();
313
314 let sig = None.or_else(|| {
315 for param in closure.params().children() {
316 match param {
317 ast::Param::Pos(ast::Pattern::Normal(ast::Expr::Ident(pos))) => {
318 let name = pos.get().clone();
319 let term = self.generate_var(name.as_str().into());
320 self.locals.insert(name, term.clone());
321 pos_all.push(term);
322 }
323 ast::Param::Pos(_pos) => {
324 pos_all.push(Ty::Any);
325 }
326 ast::Param::Named(named) => {
327 let term = self
328 .check_type_expr(module, named.expr())
329 .unwrap_or(Ty::Any);
330 named_all.insert(named.name().into(), term);
331 }
332 ast::Param::Spread(spread) => {
334 let Some(sink) = spread.sink_ident() else {
335 continue;
336 };
337 let sink_name = sink.get().clone();
338 let rest_term = self.generate_var(sink_name.as_str().into());
339 self.locals.insert(sink_name, rest_term.clone());
340 spread_right = Some(rest_term);
341 }
342 }
343 }
344
345 let body = self.check_type_expr(module, closure.body())?;
346 let sig = SigTy::new(
347 pos_all.into_iter(),
348 named_all,
349 None,
350 spread_right,
351 Some(body),
352 )
353 .into();
354
355 Some(Ty::Func(sig))
356 });
357
358 self.locals.rollback_to(snap);
359 sig
360 }
361 ast::Expr::Dict(decl) => {
362 crate::log_debug_ct!("check doc dict annotation: {decl:?}");
363 None
364 }
365 _ => None,
366 }
367 }
368}