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:
Myriad-Dreamin 2025-04-08 07:36:03 +08:00 committed by GitHub
parent ac506dcc31
commit d6bce89e68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 664 additions and 51 deletions

View file

@ -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)
}