feat: simple lint types comparing with strings (#1643)

* feat: simple lint types comparing with strings

* dev: edit message

* fix: message
This commit is contained in:
Myriad-Dreamin 2025-04-10 20:02:56 +08:00 committed by GitHub
parent 23f10a2648
commit 4265bc25bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 170 additions and 13 deletions

View file

@ -1,6 +1,11 @@
//! A linter for Typst.
use tinymist_analysis::syntax::ExprInfo;
use std::sync::Arc;
use tinymist_analysis::{
syntax::ExprInfo,
ty::{Ty, TyCtx, TypeInfo},
};
use typst::{
diag::{eco_format, EcoString, SourceDiagnostic, Tracepoint},
ecow::EcoVec,
@ -13,26 +18,32 @@ use typst::{
/// A type alias for a vector of diagnostics.
type DiagnosticVec = EcoVec<SourceDiagnostic>;
/// Performs linting check on syntax and returns a vector of diagnostics.
pub fn lint_file(expr: &ExprInfo) -> DiagnosticVec {
ExprLinter::new().lint(expr.source.root())
/// Performs linting check on file and returns a vector of diagnostics.
pub fn lint_file(expr: &ExprInfo, ti: Arc<TypeInfo>) -> DiagnosticVec {
Linter::new(ti).lint(expr.source.root())
}
struct ExprLinter {
struct Linter {
ti: Arc<TypeInfo>,
diag: DiagnosticVec,
loop_info: Option<LoopInfo>,
func_info: Option<FuncInfo>,
}
impl ExprLinter {
fn new() -> Self {
impl Linter {
fn new(ti: Arc<TypeInfo>) -> Self {
Self {
ti,
diag: EcoVec::new(),
loop_info: None,
func_info: None,
}
}
fn tctx(&self) -> &impl TyCtx {
self.ti.as_ref()
}
fn lint(mut self, node: &SyntaxNode) -> DiagnosticVec {
if let Some(markup) = node.cast::<ast::Markup>() {
self.exprs(markup.exprs());
@ -151,9 +162,42 @@ impl ExprLinter {
has_set
}
fn check_type_compare(&mut self, expr: ast::Binary<'_>) {
let op = expr.op();
if is_compare_op(op) {
let lhs = expr.lhs();
let rhs = expr.rhs();
let mut lhs = self.expr_ty(lhs);
let mut rhs = self.expr_ty(rhs);
let other_is_str = lhs.is_str(self.tctx());
if other_is_str {
(lhs, rhs) = (rhs, lhs);
}
if lhs.is_type(self.tctx()) && (other_is_str || rhs.is_str(self.tctx())) {
let msg = "comparing strings with types is deprecated";
let diag = SourceDiagnostic::warning(expr.span(), msg);
let diag = diag.with_hints([
"compare with the literal type instead".into(),
"this comparison will always return `false` since typst v0.14".into(),
]);
self.diag.push(diag);
}
}
}
fn expr_ty<'a>(&self, expr: ast::Expr<'a>) -> TypedExpr<'a> {
TypedExpr {
expr,
ty: self.ti.type_of_span(expr.span()),
}
}
}
impl DataFlowVisitor for ExprLinter {
impl DataFlowVisitor for Linter {
fn exprs<'a>(&mut self, exprs: impl DoubleEndedIterator<Item = ast::Expr<'a>>) -> Option<()> {
for expr in exprs {
self.expr(expr);
@ -262,10 +306,15 @@ impl DataFlowVisitor for ExprLinter {
}
Some(())
}
fn binary(&mut self, expr: ast::Binary<'_>) -> Option<()> {
self.check_type_compare(expr);
self.exprs([expr.lhs(), expr.rhs()].into_iter())
}
}
struct LateFuncLinter<'a> {
linter: &'a mut ExprLinter,
linter: &'a mut Linter,
func_info: FuncInfo,
return_block_info: Option<ReturnBlockInfo>,
}
@ -753,6 +802,28 @@ impl<'a> Block<'a> {
}
}
#[derive(Debug, Clone)]
struct TypedExpr<'a> {
expr: ast::Expr<'a>,
ty: Option<Ty>,
}
impl TypedExpr<'_> {
fn is_str(&self, ctx: &impl TyCtx) -> bool {
self.ty
.as_ref()
.map(|ty| ty.is_str(ctx))
.unwrap_or_else(|| matches!(self.expr, ast::Expr::Str(..)))
}
fn is_type(&self, ctx: &impl TyCtx) -> bool {
self.ty
.as_ref()
.map(|ty| ty.is_type(ctx))
.unwrap_or_default()
}
}
enum BuggyBlockLoc<'a> {
Show(ast::ShowRule<'a>),
IfTrue(ast::Conditional<'a>),
@ -830,3 +901,8 @@ impl BuggyBlockLoc<'_> {
fn is_show_set(it: ast::Expr) -> bool {
matches!(it, ast::Expr::Set(..) | ast::Expr::Show(..))
}
fn is_compare_op(op: ast::BinOp) -> bool {
use ast::BinOp::*;
matches!(op, Lt | Leq | Gt | Geq | Eq | Neq)
}