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
|
@ -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()));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue