feat: resolve definitions with dynamic analysis (#1904)
Some checks failed
tinymist::ci / Duplicate Actions Detection (push) Has been cancelled
tinymist::ci / Check Clippy, Formatting, Completion, Documentation, and Tests (Linux) (push) Has been cancelled
tinymist::ci / Check Minimum Rust version and Tests (Windows) (push) Has been cancelled
tinymist::ci / prepare-build (push) Has been cancelled
tinymist::gh_pages / build-gh-pages (push) Has been cancelled
tinymist::ci / build-vsc-assets (push) Has been cancelled
tinymist::ci / build-vscode (push) Has been cancelled
tinymist::ci / build-vscode-others (push) Has been cancelled
tinymist::ci / publish-vscode (push) Has been cancelled
tinymist::ci / E2E Tests (darwin-arm64 on macos-latest) (push) Has been cancelled
tinymist::ci / E2E Tests (linux-x64 on ubuntu-22.04) (push) Has been cancelled
tinymist::ci / E2E Tests (linux-x64 on ubuntu-latest) (push) Has been cancelled
tinymist::ci / E2E Tests (win32-x64 on windows-2022) (push) Has been cancelled
tinymist::ci / E2E Tests (win32-x64 on windows-latest) (push) Has been cancelled
tinymist::ci / build-binary (push) Has been cancelled

* feat: dyn resolve targets

* fix: test cases

* feat: static analysis again based on dyn analysis result
This commit is contained in:
Myriad-Dreamin 2025-07-15 01:15:33 +08:00 committed by GitHub
parent 35e8f447b0
commit 1359e9975b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 218 additions and 69 deletions

View file

@ -52,6 +52,10 @@ impl Definition {
pub(crate) fn value(&self) -> Option<Value> {
self.term.as_ref()?.value()
}
pub(crate) fn from_value(value: Value, name: impl FnOnce() -> Option<StrRef>) -> Option<Self> {
value_to_def(value, name)
}
}
trait HasNameRange {
@ -336,7 +340,7 @@ static WHERE_FUNC: LazyLock<Option<&'static Func>> = LazyLock::new(|| {
Some(func)
});
fn value_to_def(value: Value, name: impl FnOnce() -> Option<Interned<str>>) -> Option<Definition> {
fn value_to_def(value: Value, name: impl FnOnce() -> Option<StrRef>) -> Option<Definition> {
let val = Ty::Value(InsTy::new(value.clone()));
Some(match value {
Value::Func(func) => {

View file

@ -10,6 +10,7 @@ use parking_lot::Mutex;
use rustc_hash::FxHashMap;
use tinymist_analysis::docs::DocString;
use tinymist_analysis::stats::AllocStats;
use tinymist_analysis::syntax::classify_def_loosely;
use tinymist_analysis::ty::term_value;
use tinymist_analysis::{analyze_expr_, analyze_import_};
use tinymist_lint::LintInfo;
@ -861,12 +862,68 @@ impl SharedContext {
definition(self, source, doc, syntax)
}
pub(crate) fn type_of_span(self: &Arc<Self>, span: Span) -> Option<Ty> {
self.type_of_span_(&self.source_by_id(span.id()?).ok()?, span)
pub(crate) fn def_of_syntax_or_dyn(
self: &Arc<Self>,
source: &Source,
doc: Option<&TypstDocument>,
syntax: SyntaxClass,
) -> Option<Definition> {
let def = self.def_of_syntax(source, doc, syntax.clone());
match def.as_ref().map(|d| d.decl.kind()) {
// todo: DefKind::Function
Some(DefKind::Reference | DefKind::Module | DefKind::Function) => return def,
Some(DefKind::Struct | DefKind::Constant | DefKind::Variable) | None => {}
}
// Checks that we resolved a high-equality definition.
let know_ty_well = def
.as_ref()
.and_then(|d| self.simplified_type_of_span(d.decl.span()))
.filter(|ty| !matches!(ty, Ty::Any))
.is_some();
if know_ty_well {
return def;
}
let def_ref = def.as_ref();
let def_name = || Some(def_ref?.name().clone());
let dyn_def = self
.analyze_expr(syntax.node())
.iter()
.find_map(|(value, _)| {
let def = Definition::from_value(value.clone(), def_name)?;
None.or_else(|| {
let source = self.source_by_id(def.decl.file_id()?).ok()?;
let node = LinkedNode::new(source.root()).find(def.decl.span())?;
let def_at_the_span = classify_def_loosely(node)?;
self.def_of_span(&source, doc, def_at_the_span.name()?.span())
})
.or(Some(def))
});
// Uses the dynamic definition or the fallback definition.
dyn_def.or(def)
}
pub(crate) fn type_of_span_(self: &Arc<Self>, source: &Source, span: Span) -> Option<Ty> {
self.type_check(source).type_of_span(span)
pub(crate) fn simplified_type_of_span(self: &Arc<Self>, span: Span) -> Option<Ty> {
let source = self.source_by_id(span.id()?).ok()?;
let (ti, ty) = self.type_of_span_(&source, span)?;
Some(ti.simplify(ty, false))
}
pub(crate) fn type_of_span(self: &Arc<Self>, span: Span) -> Option<Ty> {
let source = self.source_by_id(span.id()?).ok()?;
Some(self.type_of_span_(&source, span)?.1)
}
pub(crate) fn type_of_span_(
self: &Arc<Self>,
source: &Source,
span: Span,
) -> Option<(Arc<TypeInfo>, Ty)> {
let ti = self.type_check(source);
let ty = ti.type_of_span(span)?;
Some((ti, ty))
}
pub(crate) fn post_type_of_node(self: &Arc<Self>, node: LinkedNode) -> Option<Ty> {
@ -889,6 +946,24 @@ impl SharedContext {
super::sig_of_type(self, ti, ty)
}
pub(crate) fn sig_of_type_or_dyn(
self: &Arc<Self>,
ti: &TypeInfo,
callee_ty: Ty,
callee: &SyntaxNode,
) -> Option<Signature> {
self.sig_of_type(ti, callee_ty).or_else(|| {
self.analyze_expr(callee).iter().find_map(|(value, _)| {
let Value::Func(callee) = value else {
return None;
};
// Converts with cache
analyze_signature(self, SignatureTarget::Runtime(callee.clone()))
})
})
}
/// Try to find imported target from the current source file.
/// This function will try to resolves target statically.
///

View file

@ -222,12 +222,12 @@ impl<'a> PostTypeChecker<'a> {
target,
is_set,
} => {
let callee = self.check_or(callee, context_ty)?;
let callee_ty = self.check_or(callee, context_ty)?;
crate::log_debug_ct!(
"post check call target: ({callee:?})::{target:?} is_set: {is_set}"
"post check call target: ({callee_ty:?})::{target:?} is_set: {is_set}"
);
let sig = self.ctx.sig_of_type(self.info, callee)?;
let sig = self.ctx.sig_of_type_or_dyn(self.info, callee_ty, callee)?;
crate::log_debug_ct!("post check call sig: {target:?} {sig:?}");
let mut resp = SignatureReceiver::default();

View file

@ -0,0 +1,32 @@
/// path: jsx-typings.typ
/// Creates a counter component.
///
/// - initialCounter (int): The initial value for the counter.
/// -> content
#let Counter(initialCounter: 0) = none
#let modules = (
"$components/Counter.astro": (
Counter: Counter,
),
)
-----
/// path: jsx-runtime.typ
#let typing-path = sys.inputs.at("x-jsx-typings", default: "jsx-typings.typ")
#import typing-path as typings
#let require = (it, hint: none) => typings.modules.at(it)
-----
/// contains: initialCounter
#import "jsx-runtime.typ": require
#let (Counter: Counter) = require("$components/Counter.astro");
#Counter(
/* range 0..1 */
)

View file

@ -0,0 +1,38 @@
---
source: crates/tinymist-query/src/completion.rs
description: Completion on / (142..143)
expression: "JsonRepr::new_pure(results)"
input_file: crates/tinymist-query/src/fixtures/completion/func_jsx_import.typ
---
[
{
"isIncomplete": false,
"items": [
{
"command": {
"command": "tinymist.triggerSuggestAndParameterHints",
"title": ""
},
"kind": 5,
"label": "initialCounter",
"labelDetails": {
"description": "0 | int"
},
"sortText": "000",
"textEdit": {
"newText": "initialCounter: ${1:}",
"range": {
"end": {
"character": 2,
"line": 6
},
"start": {
"character": 2,
"line": 6
}
}
}
}
]
}
]

View file

@ -0,0 +1,30 @@
/// path: jsx-typings.typ
/// Creates a counter component.
///
/// - initialCounter (int): The initial value for the counter.
/// -> content
#let Counter(initialCounter: 0) = none
#let modules = (
"$components/Counter.astro": (
Counter: Counter,
),
)
-----
/// path: jsx-runtime.typ
#let typing-path = sys.inputs.at("x-jsx-typings", default: "jsx-typings.typ")
#import typing-path as typings
#let require = (it, hint: none) => typings.modules.at(it)
-----
#import "jsx-runtime.typ": require
#let (Counter: Counter) = require("$components/Counter.astro");
#(/* ident after */ Counter)
#Counter(initialCounter: 0)

View file

@ -0,0 +1,28 @@
---
source: crates/tinymist-query/src/hover.rs
expression: content
input_file: crates/tinymist-query/src/fixtures/hover/jsx.typ
---
Range: 4:20:4:27
```typc
let Counter(
initialCounter: int = 0,
) = content | none;
```
======
Creates a counter component.
# Named Parameters
## initialCounter
```typc
type: 0 | int
```
The initial value for the counter.

View file

@ -1,58 +0,0 @@
---
source: crates/tinymist-query/src/hover.rs
expression: content
input_file: crates/tinymist-query/src/fixtures/playground/base.typ
---
Range: 5:20:5:26
```typc
let my-fun(
note: any,
mode: str = "typ",
setting: (any) => any = Closure(..),
) = none;
```
======
```
failed to parse docs: error: unclosed delimiter
┌─ /dummy-root/__wrap_md_main.typ:2:0
2 │ *
│ ^
```
```typ
*
```
# Positional Parameters
## note
```typc
type:
```
# Named Parameters
## mode
```typc
type: "typ"
```
## setting (named)
```typc
type: (any) => any
```

View file

@ -32,7 +32,7 @@ impl StatefulRequest for GotoDefinitionRequest {
let syntax = ctx.classify_for_decl(&source, self.position)?;
let origin_selection_range = ctx.to_lsp_range(syntax.node().range(), &source);
let def = ctx.def_of_syntax(&source, doc, syntax)?;
let def = ctx.def_of_syntax_or_dyn(&source, doc, syntax)?;
let fid = def.file_id()?;
let name_range = def.name_range(ctx.shared()).unwrap_or_default();

View file

@ -120,7 +120,7 @@ impl HoverWorker<'_> {
let syntax = classify_syntax(leaf.clone(), self.cursor)?;
let def = self
.ctx
.def_of_syntax(&self.source, self.doc.as_ref(), syntax.clone())?;
.def_of_syntax_or_dyn(&self.source, self.doc.as_ref(), syntax.clone())?;
use Decl::*;
match def.decl.as_ref() {

View file

@ -38,7 +38,7 @@ impl SemanticRequest for SignatureHelpRequest {
};
let syntax = classify_syntax(callee, cursor)?;
let def = ctx.def_of_syntax(&source, None, syntax)?;
let def = ctx.def_of_syntax_or_dyn(&source, None, syntax)?;
let sig = ctx.sig_of_def(def.clone())?;
crate::log_debug_ct!("got signature {sig:?}");