Skip to content

Commit e8feab8

Browse files
committed
lint: Add dylint-based linting framework
1 parent 219023e commit e8feab8

File tree

10 files changed

+655
-63
lines changed

10 files changed

+655
-63
lines changed

Cargo.lock

Lines changed: 498 additions & 63 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ members = [
1010
"lang/derive/*",
1111
"lang/syn",
1212
"spl",
13+
"lints/*",
1314
]
1415
exclude = ["tests/cfo/deps/openbook-dex", "tests/swap/deps/openbook-dex"]
1516
resolver = "2"

cli/src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ use std::sync::LazyLock;
4545

4646
mod checks;
4747
pub mod config;
48+
mod lint;
4849
pub mod rust_template;
4950

5051
// Version of the docker image.
@@ -350,6 +351,16 @@ pub enum Command {
350351
#[clap(value_enum)]
351352
shell: clap_complete::Shell,
352353
},
354+
/// Run Anchor lints on this workspace.
355+
Lint {
356+
// TODO: Once lints are published to GitHub, we shoud load from there by default
357+
/// Path containing lint library packages
358+
#[clap(long)]
359+
path: String,
360+
/// Subdirectories of the `--path` argument containing lint library packages
361+
#[clap(long)]
362+
pattern: String,
363+
},
353364
}
354365

355366
#[derive(Debug, Parser)]
@@ -915,6 +926,7 @@ fn process_command(opts: Opts) -> Result<()> {
915926
);
916927
Ok(())
917928
}
929+
Command::Lint { path, pattern } => lint::run(&path, &pattern),
918930
}
919931
}
920932

cli/src/lint.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use std::process::Command;
2+
3+
use anyhow::{bail, Context, Result};
4+
5+
/// Runs Anchor-specific lints on the workspace
6+
pub fn run(path: &str, pattern: &str) -> Result<()> {
7+
ensure_dylint()?;
8+
let status = Command::new("cargo")
9+
.args(["dylint", "--path", path, "--pattern", pattern])
10+
.status()
11+
.context("executing dylint")?;
12+
if !status.success() {
13+
bail!("dylint did not execute successfully");
14+
}
15+
Ok(())
16+
}
17+
18+
/// Ensures dylint is installed
19+
fn ensure_dylint() -> Result<()> {
20+
let output = Command::new("cargo")
21+
.arg("install")
22+
.arg("--list")
23+
.output()?;
24+
let mut has_cargo_dylint = false;
25+
let mut has_dylint_link = false;
26+
for line in String::from_utf8(output.stdout)
27+
.context("parsing `cargo install --list` output")?
28+
.lines()
29+
{
30+
let line = line.trim();
31+
if line == "cargo-dylint" {
32+
has_cargo_dylint = true;
33+
} else if line == "dylint-link" {
34+
has_dylint_link = true;
35+
}
36+
if has_cargo_dylint && has_dylint_link {
37+
break;
38+
}
39+
}
40+
if has_cargo_dylint && has_dylint_link {
41+
return Ok(());
42+
}
43+
44+
eprintln!("Installing required packages");
45+
let mut cmd = Command::new("cargo");
46+
cmd.arg("install");
47+
if !has_cargo_dylint {
48+
cmd.arg("cargo-dylint");
49+
}
50+
if !has_dylint_link {
51+
cmd.arg("dylint-link");
52+
}
53+
54+
if !cmd.status().context("installing dylint")?.success() {
55+
bail!("installing dylint failed");
56+
}
57+
Ok(())
58+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[target.'cfg(all())']
2+
rustflags = ["-C", "linker=dylint-link"]
3+
4+
# For Rust versions 1.74.0 and onward, the following alternative can be used
5+
# (see https://github.com/rust-lang/cargo/pull/12535):
6+
# linker = "dylint-link"

lints/example_lint/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/target

lints/example_lint/Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "example_lint"
3+
version = "0.1.0"
4+
authors = ["authors go here"]
5+
description = "description goes here"
6+
edition = "2024"
7+
publish = false
8+
9+
[lib]
10+
crate-type = ["cdylib"]
11+
12+
[dependencies]
13+
clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "20ce69b9a63bcd2756cd906fe0964d1e901e042a" }
14+
dylint_linting = "5.0.0"
15+
16+
[dev-dependencies]
17+
dylint_testing = "5.0.0"
18+
19+
[package.metadata.rust-analyzer]
20+
rustc_private = true

lints/example_lint/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# `example_lint`
2+
3+
### What it does
4+
5+
Identifies `msg!("Hello, world!")` instances.
6+
7+
### Why is this bad?
8+
9+
It isn't, this is just a demo.
10+
11+
### Example
12+
13+
```rust
14+
msg!("Hello, world!");
15+
```
16+
17+
Use instead:
18+
19+
```rust
20+
msg!("Goodbye, world!");
21+
```

lints/example_lint/rust-toolchain

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[toolchain]
2+
channel = "nightly-2025-09-18"
3+
components = ["llvm-tools-preview", "rustc-dev"]

lints/example_lint/src/lib.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#![feature(rustc_private)]
2+
#![warn(unused_extern_crates)]
3+
4+
extern crate rustc_hir;
5+
6+
use clippy_utils::diagnostics::span_lint;
7+
use rustc_hir::{Expr, ExprKind, QPath};
8+
use rustc_lint::{LateContext, LateLintPass};
9+
10+
dylint_linting::declare_late_lint! {
11+
#[doc = include_str!("../README.md")]
12+
pub EXAMPLE_LINT,
13+
Warn,
14+
"use of `msg!(\"Hello, world!\")"
15+
}
16+
17+
impl<'tcx> LateLintPass<'tcx> for ExampleLint {
18+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
19+
if let ExprKind::Call(target, args) = expr.kind
20+
&& let ExprKind::Path(QPath::Resolved(_, path)) = target.kind
21+
&& let Some(func) = &path.segments.last()
22+
&& func.ident.as_str() == "sol_log"
23+
&& let [arg] = &args
24+
&& let ExprKind::Lit(lit) = arg.kind
25+
&& lit.node.str().is_some_and(|s| s.as_str() == "Hello, world!")
26+
{
27+
span_lint(cx, EXAMPLE_LINT, expr.span.source_callsite(), "Use of `msg!(\"Hello, world!\")`");
28+
}
29+
}
30+
}
31+
32+
#[test]
33+
fn ui() {
34+
dylint_testing::ui_test(env!("CARGO_PKG_NAME"), "ui");
35+
}

0 commit comments

Comments
 (0)