mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-03 17:58:17 +00:00
feat: lint on bug-like show/set rules (#1634)
* feat: init linter * feat: init structure * feat: init tests * feat: warn on bad set rules * feat: refactor struct * feat: run check * chore: update message * chore: add hints
This commit is contained in:
parent
ac506dcc31
commit
d6bce89e68
18 changed files with 664 additions and 51 deletions
31
crates/tinymist-lint/Cargo.toml
Normal file
31
crates/tinymist-lint/Cargo.toml
Normal file
|
@ -0,0 +1,31 @@
|
|||
[package]
|
||||
name = "tinymist-lint"
|
||||
description = "A linter for Typst."
|
||||
categories = ["compilers"]
|
||||
keywords = ["api", "language", "typst"]
|
||||
authors.workspace = true
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
typst-library.workspace = true
|
||||
typst.workspace = true
|
||||
tinymist-std.workspace = true
|
||||
tinymist-analysis.workspace = true
|
||||
tinymist-world = { workspace = true, features = ["system"] }
|
||||
parking_lot.workspace = true
|
||||
comemo.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
base64.workspace = true
|
||||
rayon.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
insta.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
394
crates/tinymist-lint/src/lib.rs
Normal file
394
crates/tinymist-lint/src/lib.rs
Normal file
|
@ -0,0 +1,394 @@
|
|||
//! A linter for Typst.
|
||||
|
||||
use typst::{
|
||||
diag::{eco_format, EcoString, SourceDiagnostic},
|
||||
ecow::EcoVec,
|
||||
syntax::{
|
||||
ast::{self, AstNode},
|
||||
Source, SyntaxNode,
|
||||
},
|
||||
};
|
||||
|
||||
/// A type alias for a vector of diagnostics.
|
||||
type DiagnosticVec = EcoVec<SourceDiagnostic>;
|
||||
|
||||
/// Lints a Typst source and returns a vector of diagnostics.
|
||||
pub fn lint_source(source: &Source) -> DiagnosticVec {
|
||||
SourceLinter::new().lint(source.root())
|
||||
}
|
||||
|
||||
struct SourceLinter {
|
||||
diag: DiagnosticVec,
|
||||
}
|
||||
|
||||
impl SourceLinter {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
diag: EcoVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn lint(mut self, node: &SyntaxNode) -> DiagnosticVec {
|
||||
if let Some(markup) = node.cast::<ast::Markup>() {
|
||||
self.exprs(markup.exprs());
|
||||
} else if let Some(expr) = node.cast() {
|
||||
self.expr(expr);
|
||||
}
|
||||
|
||||
self.diag
|
||||
}
|
||||
|
||||
fn exprs<'a>(&mut self, exprs: impl Iterator<Item = ast::Expr<'a>>) -> Option<()> {
|
||||
for expr in exprs {
|
||||
self.expr(expr);
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn exprs_untyped(&mut self, to_untyped: &SyntaxNode) -> Option<()> {
|
||||
for expr in to_untyped.children() {
|
||||
if let Some(expr) = expr.cast() {
|
||||
self.expr(expr);
|
||||
}
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn expr(&mut self, node: ast::Expr) -> Option<()> {
|
||||
match node {
|
||||
ast::Expr::Parenthesized(expr) => self.expr(expr.expr()),
|
||||
ast::Expr::Code(expr) => self.exprs(expr.body().exprs()),
|
||||
ast::Expr::Content(expr) => self.exprs(expr.body().exprs()),
|
||||
ast::Expr::Equation(expr) => self.exprs(expr.body().exprs()),
|
||||
ast::Expr::Math(expr) => self.exprs(expr.exprs()),
|
||||
|
||||
ast::Expr::Text(..) => None,
|
||||
ast::Expr::Space(..) => None,
|
||||
ast::Expr::Linebreak(..) => None,
|
||||
ast::Expr::Parbreak(..) => None,
|
||||
ast::Expr::Escape(..) => None,
|
||||
ast::Expr::Shorthand(..) => None,
|
||||
ast::Expr::SmartQuote(..) => None,
|
||||
ast::Expr::Raw(..) => None,
|
||||
ast::Expr::Link(..) => None,
|
||||
|
||||
ast::Expr::Label(..) => None,
|
||||
ast::Expr::Ref(..) => None,
|
||||
ast::Expr::None(..) => None,
|
||||
ast::Expr::Auto(..) => None,
|
||||
ast::Expr::Bool(..) => None,
|
||||
ast::Expr::Int(..) => None,
|
||||
ast::Expr::Float(..) => None,
|
||||
ast::Expr::Numeric(..) => None,
|
||||
ast::Expr::Str(..) => None,
|
||||
ast::Expr::MathText(..) => None,
|
||||
ast::Expr::MathShorthand(..) => None,
|
||||
ast::Expr::MathAlignPoint(..) => None,
|
||||
ast::Expr::MathPrimes(..) => None,
|
||||
ast::Expr::MathRoot(..) => None,
|
||||
|
||||
ast::Expr::Strong(content) => self.exprs(content.body().exprs()),
|
||||
ast::Expr::Emph(content) => self.exprs(content.body().exprs()),
|
||||
ast::Expr::Heading(content) => self.exprs(content.body().exprs()),
|
||||
ast::Expr::List(content) => self.exprs(content.body().exprs()),
|
||||
ast::Expr::Enum(content) => self.exprs(content.body().exprs()),
|
||||
ast::Expr::Term(content) => {
|
||||
self.exprs(content.term().exprs());
|
||||
self.exprs(content.description().exprs())
|
||||
}
|
||||
ast::Expr::MathDelimited(content) => self.exprs(content.body().exprs()),
|
||||
ast::Expr::MathAttach(..) | ast::Expr::MathFrac(..) => {
|
||||
self.exprs_untyped(node.to_untyped())
|
||||
}
|
||||
|
||||
ast::Expr::Ident(expr) => self.ident(expr),
|
||||
ast::Expr::MathIdent(expr) => self.math_ident(expr),
|
||||
ast::Expr::Array(expr) => self.array(expr),
|
||||
ast::Expr::Dict(expr) => self.dict(expr),
|
||||
ast::Expr::Unary(expr) => self.unary(expr),
|
||||
ast::Expr::Binary(expr) => self.binary(expr),
|
||||
ast::Expr::FieldAccess(expr) => self.field_access(expr),
|
||||
ast::Expr::FuncCall(expr) => self.func_call(expr),
|
||||
ast::Expr::Closure(expr) => self.closure(expr),
|
||||
ast::Expr::Let(expr) => self.let_binding(expr),
|
||||
ast::Expr::DestructAssign(expr) => self.destruct_assign(expr),
|
||||
ast::Expr::Set(expr) => self.set(expr),
|
||||
ast::Expr::Show(expr) => self.show(expr),
|
||||
ast::Expr::Contextual(expr) => self.contextual(expr),
|
||||
ast::Expr::Conditional(expr) => self.conditional(expr),
|
||||
ast::Expr::While(expr) => self.while_loop(expr),
|
||||
ast::Expr::For(expr) => self.for_loop(expr),
|
||||
ast::Expr::Import(expr) => self.import(expr),
|
||||
ast::Expr::Include(expr) => self.include(expr),
|
||||
ast::Expr::Break(expr) => self.loop_break(expr),
|
||||
ast::Expr::Continue(expr) => self.loop_continue(expr),
|
||||
ast::Expr::Return(expr) => self.func_return(expr),
|
||||
}
|
||||
}
|
||||
|
||||
fn ident(&mut self, _expr: ast::Ident<'_>) -> Option<()> {
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn math_ident(&mut self, _expr: ast::MathIdent<'_>) -> Option<()> {
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn import(&mut self, _expr: ast::ModuleImport<'_>) -> Option<()> {
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn include(&mut self, _expr: ast::ModuleInclude<'_>) -> Option<()> {
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn array(&mut self, expr: ast::Array<'_>) -> Option<()> {
|
||||
self.exprs_untyped(expr.to_untyped())
|
||||
}
|
||||
|
||||
fn dict(&mut self, expr: ast::Dict<'_>) -> Option<()> {
|
||||
self.exprs_untyped(expr.to_untyped())
|
||||
}
|
||||
|
||||
fn unary(&mut self, expr: ast::Unary<'_>) -> Option<()> {
|
||||
self.expr(expr.expr())
|
||||
}
|
||||
|
||||
fn binary(&mut self, expr: ast::Binary<'_>) -> Option<()> {
|
||||
self.expr(expr.lhs());
|
||||
self.expr(expr.rhs())
|
||||
}
|
||||
|
||||
fn field_access(&mut self, expr: ast::FieldAccess<'_>) -> Option<()> {
|
||||
self.expr(expr.target())
|
||||
}
|
||||
|
||||
fn func_call(&mut self, expr: ast::FuncCall<'_>) -> Option<()> {
|
||||
self.exprs_untyped(expr.args().to_untyped());
|
||||
self.expr(expr.callee())
|
||||
}
|
||||
|
||||
fn closure(&mut self, expr: ast::Closure<'_>) -> Option<()> {
|
||||
self.exprs_untyped(expr.params().to_untyped());
|
||||
self.expr(expr.body())
|
||||
}
|
||||
|
||||
fn let_binding(&mut self, expr: ast::LetBinding<'_>) -> Option<()> {
|
||||
self.expr(expr.init()?)
|
||||
}
|
||||
|
||||
fn destruct_assign(&mut self, expr: ast::DestructAssignment<'_>) -> Option<()> {
|
||||
self.expr(expr.value())
|
||||
}
|
||||
|
||||
fn set(&mut self, expr: ast::SetRule<'_>) -> Option<()> {
|
||||
if let Some(target) = expr.condition() {
|
||||
self.expr(target);
|
||||
}
|
||||
self.exprs_untyped(expr.args().to_untyped());
|
||||
self.expr(expr.target())
|
||||
}
|
||||
|
||||
fn show(&mut self, expr: ast::ShowRule<'_>) -> Option<()> {
|
||||
if let Some(target) = expr.selector() {
|
||||
self.expr(target);
|
||||
}
|
||||
let transform = expr.transform();
|
||||
match transform {
|
||||
ast::Expr::Code(..) | ast::Expr::Content(..) => {
|
||||
self.buggy_show(transform, BuggyShowLoc::Show(expr))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
self.expr(transform)
|
||||
}
|
||||
|
||||
fn contextual(&mut self, expr: ast::Contextual<'_>) -> Option<()> {
|
||||
self.expr(expr.body())
|
||||
}
|
||||
|
||||
fn conditional(&mut self, expr: ast::Conditional<'_>) -> Option<()> {
|
||||
self.expr(expr.condition());
|
||||
|
||||
let if_body = expr.if_body();
|
||||
self.buggy_show(if_body, BuggyShowLoc::IfTrue(expr));
|
||||
self.expr(if_body);
|
||||
|
||||
if let Some(else_body) = expr.else_body() {
|
||||
self.buggy_show(else_body, BuggyShowLoc::IfFalse(expr));
|
||||
self.expr(else_body);
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn while_loop(&mut self, expr: ast::WhileLoop<'_>) -> Option<()> {
|
||||
self.expr(expr.condition());
|
||||
let body = expr.body();
|
||||
self.buggy_show(body, BuggyShowLoc::While(expr));
|
||||
self.expr(body)
|
||||
}
|
||||
|
||||
fn for_loop(&mut self, expr: ast::ForLoop<'_>) -> Option<()> {
|
||||
self.expr(expr.iterable());
|
||||
let body = expr.body();
|
||||
self.buggy_show(body, BuggyShowLoc::For(expr));
|
||||
self.expr(body)
|
||||
}
|
||||
|
||||
fn loop_break(&mut self, _expr: ast::LoopBreak<'_>) -> Option<()> {
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn loop_continue(&mut self, _expr: ast::LoopContinue<'_>) -> Option<()> {
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn func_return(&mut self, _expr: ast::FuncReturn<'_>) -> Option<()> {
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn buggy_show(&mut self, expr: ast::Expr, loc: BuggyShowLoc) -> Option<()> {
|
||||
if self.only_set(expr) {
|
||||
let sets = match expr {
|
||||
ast::Expr::Code(block) => block
|
||||
.body()
|
||||
.exprs()
|
||||
.filter(|it| is_show_set(*it))
|
||||
.collect::<Vec<_>>(),
|
||||
ast::Expr::Content(block) => block
|
||||
.body()
|
||||
.exprs()
|
||||
.filter(|it| is_show_set(*it))
|
||||
.collect::<Vec<_>>(),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
for (idx, set) in sets.iter().enumerate() {
|
||||
let msg = match set {
|
||||
ast::Expr::Set(..) => "This set statement doesn't take effect.",
|
||||
ast::Expr::Show(..) => "This show statement doesn't take effect.",
|
||||
_ => continue,
|
||||
};
|
||||
let mut warning = SourceDiagnostic::warning(set.span(), msg);
|
||||
if idx == 0 {
|
||||
warning.hint(loc.hint(*set));
|
||||
}
|
||||
|
||||
self.diag.push(warning);
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn only_set(&mut self, expr: ast::Expr) -> bool {
|
||||
let mut has_set = false;
|
||||
|
||||
match expr {
|
||||
ast::Expr::Code(block) => {
|
||||
for it in block.body().exprs() {
|
||||
if is_show_set(it) {
|
||||
has_set = true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
ast::Expr::Content(block) => {
|
||||
for it in block.body().exprs() {
|
||||
if is_show_set(it) {
|
||||
has_set = true;
|
||||
} else if !it.to_untyped().kind().is_trivia() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
has_set
|
||||
}
|
||||
}
|
||||
|
||||
enum BuggyShowLoc<'a> {
|
||||
Show(ast::ShowRule<'a>),
|
||||
IfTrue(ast::Conditional<'a>),
|
||||
IfFalse(ast::Conditional<'a>),
|
||||
While(ast::WhileLoop<'a>),
|
||||
For(ast::ForLoop<'a>),
|
||||
}
|
||||
impl BuggyShowLoc<'_> {
|
||||
fn hint(&self, show_set: ast::Expr<'_>) -> EcoString {
|
||||
match self {
|
||||
BuggyShowLoc::Show(show_parent) => {
|
||||
if let ast::Expr::Show(show) = show_set {
|
||||
eco_format!(
|
||||
"consider changing parent to `show {}: it => {{ {}; it }}`",
|
||||
match show_parent.selector() {
|
||||
Some(selector) => selector.to_untyped().clone().into_text(),
|
||||
None => "".into(),
|
||||
},
|
||||
show.to_untyped().clone().into_text()
|
||||
)
|
||||
} else {
|
||||
eco_format!(
|
||||
"consider changing parent to `show {}: {}`",
|
||||
match show_parent.selector() {
|
||||
Some(selector) => selector.to_untyped().clone().into_text(),
|
||||
None => "".into(),
|
||||
},
|
||||
show_set.to_untyped().clone().into_text()
|
||||
)
|
||||
}
|
||||
}
|
||||
BuggyShowLoc::IfTrue(conditional) | BuggyShowLoc::IfFalse(conditional) => {
|
||||
let neg = if matches!(self, BuggyShowLoc::IfTrue(..)) {
|
||||
""
|
||||
} else {
|
||||
"not "
|
||||
};
|
||||
if let ast::Expr::Show(show) = show_set {
|
||||
eco_format!(
|
||||
"consider changing parent to `show {}: if {neg}({}) {{ .. }}`",
|
||||
match show.selector() {
|
||||
Some(selector) => selector.to_untyped().clone().into_text(),
|
||||
None => "".into(),
|
||||
},
|
||||
conditional.condition().to_untyped().clone().into_text()
|
||||
)
|
||||
} else {
|
||||
eco_format!(
|
||||
"consider changing parent to `{} if {neg}({})`",
|
||||
show_set.to_untyped().clone().into_text(),
|
||||
conditional.condition().to_untyped().clone().into_text()
|
||||
)
|
||||
}
|
||||
}
|
||||
BuggyShowLoc::While(w) => {
|
||||
eco_format!(
|
||||
"consider changing parent to `show: it => if {} {{ {}; it }}`",
|
||||
w.condition().to_untyped().clone().into_text(),
|
||||
show_set.to_untyped().clone().into_text()
|
||||
)
|
||||
}
|
||||
BuggyShowLoc::For(f) => {
|
||||
eco_format!(
|
||||
"consider changing parent to `show: {}.fold(it => it, (style-it, {}) => it => {{ {}; style-it(it) }})`",
|
||||
f.iterable().to_untyped().clone().into_text(),
|
||||
f.pattern().to_untyped().clone().into_text(),
|
||||
show_set.to_untyped().clone().into_text()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_show_set(it: ast::Expr) -> bool {
|
||||
matches!(it, ast::Expr::Set(..) | ast::Expr::Show(..))
|
||||
}
|
|
@ -48,6 +48,7 @@ tinymist-analysis.workspace = true
|
|||
tinymist-derive.workspace = true
|
||||
tinymist-std.workspace = true
|
||||
tinymist-l10n.workspace = true
|
||||
tinymist-lint.workspace = true
|
||||
typst.workspace = true
|
||||
typst-shim.workspace = true
|
||||
unscanny.workspace = true
|
||||
|
|
|
@ -598,3 +598,27 @@ mod call_info_tests {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod lint_tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::tests::*;
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
snapshot_testing("lint", &|ctx, path| {
|
||||
let source = ctx.source_by_path(&path).unwrap();
|
||||
|
||||
let result = tinymist_lint::lint_source(&source);
|
||||
let result =
|
||||
crate::diagnostics::CheckDocWorker::new(&ctx.world, ctx.position_encoding())
|
||||
.convert_all(result.iter());
|
||||
let result = result
|
||||
.into_iter()
|
||||
.map(|(k, v)| (file_path_(&k), v))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use tinymist_project::LspWorld;
|
||||
use typst::syntax::Span;
|
||||
use tinymist_world::vfs::WorkspaceResolver;
|
||||
use typst::{diag::SourceDiagnostic, syntax::Span};
|
||||
|
||||
use crate::{prelude::*, LspWorldExt};
|
||||
|
||||
|
@ -13,15 +14,29 @@ pub type DiagnosticsMap = HashMap<Url, EcoVec<Diagnostic>>;
|
|||
type TypstDiagnostic = typst::diag::SourceDiagnostic;
|
||||
type TypstSeverity = typst::diag::Severity;
|
||||
|
||||
/// Converts a list of Typst diagnostics to LSP diagnostics,
|
||||
/// with potential refinements on the error messages.
|
||||
pub fn check_doc<'a>(
|
||||
world: &LspWorld,
|
||||
errors: impl IntoIterator<Item = &'a TypstDiagnostic>,
|
||||
position_encoding: PositionEncoding,
|
||||
) -> DiagnosticsMap {
|
||||
CheckDocWorker::new(world, position_encoding)
|
||||
.check()
|
||||
.convert_all(errors)
|
||||
}
|
||||
|
||||
/// Context for converting Typst diagnostics to LSP diagnostics.
|
||||
struct LocalDiagContext<'a> {
|
||||
pub(crate) struct CheckDocWorker<'a> {
|
||||
/// The world surface for Typst compiler.
|
||||
pub world: &'a LspWorld,
|
||||
/// The position encoding for the source.
|
||||
pub position_encoding: PositionEncoding,
|
||||
/// Results
|
||||
pub results: DiagnosticsMap,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for LocalDiagContext<'_> {
|
||||
impl std::ops::Deref for CheckDocWorker<'_> {
|
||||
type Target = LspWorld;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
|
@ -29,37 +44,64 @@ impl std::ops::Deref for LocalDiagContext<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Converts a list of Typst diagnostics to LSP diagnostics,
|
||||
/// with potential refinements on the error messages.
|
||||
pub fn convert_diagnostics<'a>(
|
||||
world: &LspWorld,
|
||||
errors: impl IntoIterator<Item = &'a TypstDiagnostic>,
|
||||
position_encoding: PositionEncoding,
|
||||
) -> DiagnosticsMap {
|
||||
let ctx = LocalDiagContext {
|
||||
world,
|
||||
position_encoding,
|
||||
};
|
||||
|
||||
let kvs = errors
|
||||
.into_iter()
|
||||
.flat_map(|error| {
|
||||
convert_diagnostic(&ctx, error)
|
||||
.map_err(move |conversion_err| {
|
||||
log::error!("could not convert Typst error to diagnostic: {conversion_err:?} error to convert: {error:?}");
|
||||
})
|
||||
});
|
||||
|
||||
let mut lookup = HashMap::new();
|
||||
for (key, val) in kvs {
|
||||
lookup.entry(key).or_insert_with(EcoVec::new).push(val);
|
||||
impl<'w> CheckDocWorker<'w> {
|
||||
/// Creates a new `CheckDocWorker` instance.
|
||||
pub fn new(world: &'w LspWorld, position_encoding: PositionEncoding) -> Self {
|
||||
Self {
|
||||
world,
|
||||
position_encoding,
|
||||
results: DiagnosticsMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
lookup
|
||||
/// Runs code check on the document.
|
||||
pub fn check(mut self) -> Self {
|
||||
for dep in self.world.depended_files() {
|
||||
if WorkspaceResolver::is_package_file(dep) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(source) = self.world.source(dep) else {
|
||||
continue;
|
||||
};
|
||||
let res = lint_source(&source);
|
||||
if !res.is_empty() {
|
||||
for diag in res {
|
||||
self.handle(&diag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Converts a list of Typst diagnostics to LSP diagnostics.
|
||||
pub fn convert_all<'a>(
|
||||
mut self,
|
||||
errors: impl IntoIterator<Item = &'a TypstDiagnostic>,
|
||||
) -> DiagnosticsMap {
|
||||
for diag in errors {
|
||||
self.handle(diag);
|
||||
}
|
||||
|
||||
self.results
|
||||
}
|
||||
|
||||
/// Converts a list of Typst diagnostics to LSP diagnostics.
|
||||
pub fn handle(&mut self, diag: &TypstDiagnostic) {
|
||||
match convert_diagnostic(self, diag) {
|
||||
Ok((uri, diagnostic)) => {
|
||||
self.results.entry(uri).or_default().push(diagnostic);
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("Failed to convert Typst diagnostic: {error:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_diagnostic(
|
||||
ctx: &LocalDiagContext,
|
||||
ctx: &CheckDocWorker,
|
||||
typst_diagnostic: &TypstDiagnostic,
|
||||
) -> anyhow::Result<(Url, Diagnostic)> {
|
||||
let typst_diagnostic = {
|
||||
|
@ -102,7 +144,7 @@ fn convert_diagnostic(
|
|||
}
|
||||
|
||||
fn tracepoint_to_relatedinformation(
|
||||
ctx: &LocalDiagContext,
|
||||
ctx: &CheckDocWorker,
|
||||
tracepoint: &Spanned<Tracepoint>,
|
||||
position_encoding: PositionEncoding,
|
||||
) -> anyhow::Result<Option<DiagnosticRelatedInformation>> {
|
||||
|
@ -127,7 +169,7 @@ fn tracepoint_to_relatedinformation(
|
|||
}
|
||||
|
||||
fn diagnostic_related_information(
|
||||
project: &LocalDiagContext,
|
||||
project: &CheckDocWorker,
|
||||
typst_diagnostic: &TypstDiagnostic,
|
||||
position_encoding: PositionEncoding,
|
||||
) -> anyhow::Result<Vec<DiagnosticRelatedInformation>> {
|
||||
|
@ -145,7 +187,7 @@ fn diagnostic_related_information(
|
|||
}
|
||||
|
||||
fn diagnostic_span_id(
|
||||
ctx: &LocalDiagContext,
|
||||
ctx: &CheckDocWorker,
|
||||
typst_diagnostic: &TypstDiagnostic,
|
||||
) -> (TypstFileId, Span) {
|
||||
iter::once(typst_diagnostic.span)
|
||||
|
@ -238,3 +280,8 @@ impl DiagnosticRefiner for OutOfRootHintRefiner {
|
|||
raw.with_hint("Cannot read file outside of project root.")
|
||||
}
|
||||
}
|
||||
|
||||
#[comemo::memoize]
|
||||
fn lint_source(source: &Source) -> EcoVec<SourceDiagnostic> {
|
||||
tinymist_lint::lint_source(source)
|
||||
}
|
||||
|
|
6
crates/tinymist-query/src/fixtures/lint/if_set.typ
Normal file
6
crates/tinymist-query/src/fixtures/lint/if_set.typ
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
#if false {
|
||||
set text(red)
|
||||
}
|
||||
|
||||
123
|
6
crates/tinymist-query/src/fixtures/lint/if_show.typ
Normal file
6
crates/tinymist-query/src/fixtures/lint/if_show.typ
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
#if false {
|
||||
show: text(red)
|
||||
}
|
||||
|
||||
123
|
6
crates/tinymist-query/src/fixtures/lint/show_set.typ
Normal file
6
crates/tinymist-query/src/fixtures/lint/show_set.typ
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
#show: {
|
||||
set text(red)
|
||||
}
|
||||
|
||||
123
|
6
crates/tinymist-query/src/fixtures/lint/show_set2.typ
Normal file
6
crates/tinymist-query/src/fixtures/lint/show_set2.typ
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
#show raw: {
|
||||
set text(red)
|
||||
}
|
||||
|
||||
123
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/analysis.rs
|
||||
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
|
||||
input_file: crates/tinymist-query/src/fixtures/lint/if_set.typ
|
||||
---
|
||||
{
|
||||
"s0.typ": [
|
||||
{
|
||||
"message": "This set statement doesn't take effect.\nHint: consider changing parent to `set text(red) if (false)`",
|
||||
"range": "1:2:1:15",
|
||||
"relatedInformation": [],
|
||||
"severity": 2,
|
||||
"source": "typst"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/analysis.rs
|
||||
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
|
||||
input_file: crates/tinymist-query/src/fixtures/lint/if_show.typ
|
||||
---
|
||||
{
|
||||
"s0.typ": [
|
||||
{
|
||||
"message": "This show statement doesn't take effect.\nHint: consider changing parent to `show : if (false) { .. }`",
|
||||
"range": "1:2:1:17",
|
||||
"relatedInformation": [],
|
||||
"severity": 2,
|
||||
"source": "typst"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/analysis.rs
|
||||
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
|
||||
input_file: crates/tinymist-query/src/fixtures/lint/show_set.typ
|
||||
---
|
||||
{
|
||||
"s0.typ": [
|
||||
{
|
||||
"message": "This set statement doesn't take effect.\nHint: consider changing parent to `show : set text(red)`",
|
||||
"range": "1:2:1:15",
|
||||
"relatedInformation": [],
|
||||
"severity": 2,
|
||||
"source": "typst"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/analysis.rs
|
||||
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
|
||||
input_file: crates/tinymist-query/src/fixtures/lint/show_set2.typ
|
||||
---
|
||||
{
|
||||
"s0.typ": [
|
||||
{
|
||||
"message": "This set statement doesn't take effect.\nHint: consider changing parent to `show raw: set text(red)`",
|
||||
"range": "1:2:1:15",
|
||||
"relatedInformation": [],
|
||||
"severity": 2,
|
||||
"source": "typst"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -477,12 +477,16 @@ impl Redact for RedactFields {
|
|||
}
|
||||
|
||||
pub(crate) fn file_path(uri: &str) -> String {
|
||||
file_path_(&lsp_types::Url::parse(uri).unwrap())
|
||||
}
|
||||
|
||||
pub(crate) fn file_path_(uri: &lsp_types::Url) -> String {
|
||||
let root = if cfg!(windows) {
|
||||
PathBuf::from("C:\\root")
|
||||
} else {
|
||||
PathBuf::from("/root")
|
||||
};
|
||||
let uri = lsp_types::Url::parse(uri).unwrap().to_file_path().unwrap();
|
||||
let uri = uri.to_file_path().unwrap();
|
||||
let abs_path = Path::new(&uri).strip_prefix(root).map(|p| p.to_owned());
|
||||
let rel_path =
|
||||
abs_path.unwrap_or_else(|_| Path::new("-").join(Path::new(&uri).iter().last().unwrap()));
|
||||
|
|
|
@ -438,27 +438,32 @@ impl CompileHandlerImpl {
|
|||
}
|
||||
|
||||
fn notify_diagnostics(&self, snap: &LspCompiledArtifact) {
|
||||
let world = snap.world();
|
||||
let dv = ProjVersion {
|
||||
id: snap.id().clone(),
|
||||
revision: world.revision().get(),
|
||||
revision: snap.world().revision().get(),
|
||||
};
|
||||
|
||||
// todo: better way to remove diagnostics
|
||||
// todo: check all errors in this file
|
||||
let valid = !world.entry_state().is_inactive();
|
||||
let diagnostics = valid.then(|| {
|
||||
let diagnostics = tinymist_query::convert_diagnostics(
|
||||
world,
|
||||
snap.diagnostics(),
|
||||
self.analysis.position_encoding,
|
||||
);
|
||||
let valid = !snap.world().entry_state().is_inactive();
|
||||
if !valid {
|
||||
self.push_diagnostics(dv, None);
|
||||
return;
|
||||
}
|
||||
|
||||
let snap = snap.clone();
|
||||
let editor_tx = self.editor_tx.clone();
|
||||
let enc = self.analysis.position_encoding;
|
||||
rayon::spawn(move || {
|
||||
let world = snap.world();
|
||||
|
||||
// todo: check all errors in this file
|
||||
let diagnostics = tinymist_query::check_doc(world, snap.diagnostics(), enc);
|
||||
|
||||
log::trace!("notify diagnostics({dv:?}): {diagnostics:#?}");
|
||||
diagnostics
|
||||
});
|
||||
|
||||
self.push_diagnostics(dv, diagnostics);
|
||||
editor_tx
|
||||
.send(EditorRequest::Diag(dv, Some(diagnostics)))
|
||||
.log_error("failed to send diagnostics");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -619,8 +624,6 @@ impl CompileHandler<LspCompilerFeat, ProjectInsStateExt> for CompileHandlerImpl
|
|||
.log_error("failed to print diagnostics");
|
||||
}
|
||||
|
||||
self.notify_diagnostics(art);
|
||||
|
||||
self.client.interrupt(LspInterrupt::Compiled(art.clone()));
|
||||
self.export.signal(art);
|
||||
|
||||
|
@ -631,6 +634,8 @@ impl CompileHandler<LspCompilerFeat, ProjectInsStateExt> for CompileHandlerImpl
|
|||
} else {
|
||||
log::info!("Project: no preview for {:?}", art.id());
|
||||
}
|
||||
|
||||
self.notify_diagnostics(art);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -177,8 +177,7 @@ async fn trace_main(
|
|||
let timings = writer.into_inner().unwrap();
|
||||
|
||||
let handle = &state.project;
|
||||
let diagnostics =
|
||||
tinymist_query::convert_diagnostics(w, diags.iter(), handle.analysis.position_encoding);
|
||||
let diagnostics = tinymist_query::check_doc(w, diags.iter(), handle.analysis.position_encoding);
|
||||
|
||||
let rpc_kind = rpc_kind.as_str();
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue