Skip to main content

x/
lint.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use anyhow::anyhow;
5use camino::Utf8Path;
6use clap::Parser;
7use nexlint::{NexLintContext, prelude::*};
8use nexlint_lints::{
9    content::*,
10    package::*,
11    project::{
12        BannedDepConfig, BannedDepType, BannedDeps, BannedDepsConfig, DirectDepDups,
13        DirectDepDupsConfig, DirectDuplicateGitDependencies,
14    },
15};
16static EXTERNAL_CRATE_DIR: &str = "external-crates/";
17static CREATE_DAPP_TEMPLATE_DIR: &str = "sdk/create-dapp/templates";
18static LICENSE_HEADER: &str = "Copyright (c) Mysten Labs, Inc.\n\
19                               SPDX-License-Identifier: Apache-2.0\n\
20                               ";
21#[derive(Debug, Parser)]
22pub struct Args {
23    #[clap(long)]
24    fail_fast: bool,
25}
26
27pub fn run(args: Args) -> crate::Result<()> {
28    let banned_deps_config = BannedDepsConfig(
29        vec![
30            (
31                "lazy_static".to_owned(),
32                BannedDepConfig {
33                    message: "use once_cell::sync::Lazy instead".to_owned(),
34                    type_: BannedDepType::Direct,
35                },
36            ),
37            (
38                "tracing-test".to_owned(),
39                BannedDepConfig {
40                    message: "you should not be testing against log lines".to_owned(),
41                    type_: BannedDepType::Always,
42                },
43            ),
44            (
45                "openssl-sys".to_owned(),
46                BannedDepConfig {
47                    message: "use rustls for TLS".to_owned(),
48                    type_: BannedDepType::Always,
49                },
50            ),
51            (
52                "actix-web".to_owned(),
53                BannedDepConfig {
54                    message: "use axum for a webframework instead".to_owned(),
55                    type_: BannedDepType::Always,
56                },
57            ),
58            (
59                "warp".to_owned(),
60                BannedDepConfig {
61                    message: "use axum for a webframework instead".to_owned(),
62                    type_: BannedDepType::Always,
63                },
64            ),
65            (
66                "pq-sys".to_owned(),
67                BannedDepConfig {
68                    message: "diesel_async asynchronous database connections instead".to_owned(),
69                    type_: BannedDepType::Always,
70                },
71            ),
72        ]
73        .into_iter()
74        .collect(),
75    );
76
77    let direct_dep_dups_config = DirectDepDupsConfig {
78        allow: vec![
79            // TODO spend the time to de-dup these direct dependencies
80            "serde_yaml".to_owned(),
81            "syn".to_owned(),
82            // Our opentelemetry integration requires that we use the same version of these packages
83            // as the opentelemetry crates.
84            "prost".to_owned(),
85            "tonic".to_owned(),
86            // jsonrpsee uses an older version of http-body
87            "http-body".to_owned(),
88            // jsonrpsee uses an older version of tower
89            "tower".to_owned(),
90            // async-graphql uses an older version of axum, axum-extra
91            "axum".to_owned(),
92            "axum-extra".to_owned(),
93            // consistent-store uses a newer version of bincode with breaking interface changes
94            "bincode".to_owned(),
95            // TODO: remove once we've migrated ethers to alloy: https://linear.app/mysten-labs/issue/BR-191
96            "reqwest".to_owned(),
97        ],
98    };
99
100    let project_linters: &[&dyn ProjectLinter] = &[
101        &BannedDeps::new(&banned_deps_config),
102        &DirectDepDups::new(&direct_dep_dups_config),
103        &DirectDuplicateGitDependencies,
104    ];
105
106    let package_linters: &[&dyn PackageLinter] = &[
107        &CrateNamesPaths,
108        &IrrelevantBuildDeps,
109        &WorkspaceLintsOptIn,
110        // This one seems to be broken
111        // &UnpublishedPackagesOnlyUsePathDependencies::new(),
112        &PublishedPackagesDontDependOnUnpublishedPackages,
113        &OnlyPublishToCratesIo,
114        &CratesInCratesDirectory,
115        // There are crates under consensus/, external-crates/.
116        // &CratesOnlyInCratesDirectory,
117    ];
118
119    let file_path_linters: &[&dyn FilePathLinter] = &[
120        // &AllowedPaths::new(DEFAULT_ALLOWED_PATHS_REGEX)?
121        ];
122
123    // allow whitespace exceptions for markdown files
124    // let whitespace_exceptions = build_exceptions(&["*.md".to_owned()])?;
125    let content_linters: &[&dyn ContentLinter] = &[
126        &LicenseHeader::new(LICENSE_HEADER),
127        &RootToml,
128        // &EofNewline::new(&whitespace_exceptions),
129        // &TrailingWhitespace::new(&whitespace_exceptions),
130    ];
131
132    let nexlint_context = NexLintContext::from_current_dir()?;
133    let engine = LintEngineConfig::new(&nexlint_context)
134        .with_project_linters(project_linters)
135        .with_package_linters(package_linters)
136        .with_file_path_linters(file_path_linters)
137        .with_content_linters(content_linters)
138        .fail_fast(args.fail_fast)
139        .build();
140
141    let results = engine.run()?;
142
143    handle_lint_results_exclude_external_crate_checks(results)
144}
145
146/// Enforces that every workspace member inherits `[workspace.lints]` from the root
147/// Cargo.toml via `[lints] workspace = true`. `cargo clippy` / `cargo xclippy` rely on
148/// this table for the project-wide lint set, so a member that doesn't opt in would
149/// silently be linted with no lints at all.
150#[derive(Debug)]
151struct WorkspaceLintsOptIn;
152
153impl Linter for WorkspaceLintsOptIn {
154    fn name(&self) -> &'static str {
155        "workspace-lints-opt-in"
156    }
157}
158
159impl PackageLinter for WorkspaceLintsOptIn {
160    fn run<'l>(
161        &self,
162        ctx: &PackageContext<'l>,
163        out: &mut LintFormatter<'l, '_>,
164    ) -> Result<RunStatus<'l>, SystemError> {
165        let manifest_path = ctx.metadata().manifest_path();
166        let contents = std::fs::read_to_string(manifest_path)
167            .map_err(|err| SystemError::io("reading manifest", err))?;
168        let manifest: toml::Value = toml::from_str(&contents)
169            .map_err(|err| SystemError::de("deserializing manifest", err))?;
170        let opted_in = manifest
171            .get("lints")
172            .and_then(|lints| lints.get("workspace"))
173            .and_then(|workspace| workspace.as_bool())
174            == Some(true);
175        if !opted_in {
176            out.write(
177                LintLevel::Error,
178                "missing `[lints] workspace = true` in Cargo.toml: all workspace members \
179                 must inherit `[workspace.lints]` so clippy applies the project lint set",
180            );
181        }
182        Ok(RunStatus::Executed)
183    }
184}
185
186/// Define custom handler so we can skip certain lints on certain files. This is a temporary till we upstream this logic
187pub fn handle_lint_results_exclude_external_crate_checks(
188    results: LintResults,
189) -> crate::Result<()> {
190    // ignore_funcs is a slice of funcs to execute against lint sources and their path
191    // if a func returns true, it means it will be ignored and not throw a lint error
192    let ignore_funcs = [
193        // legacy ignore checks
194        |source: &LintSource, path: &Utf8Path| -> bool {
195            (path.starts_with(EXTERNAL_CRATE_DIR)
196                || path.starts_with(CREATE_DAPP_TEMPLATE_DIR)
197                || path.to_string().contains("/generated/")
198                || path.to_string().contains("/proto/")
199                || path.file_name() == Some("codegen.rs"))
200                && source.name() == "license-header"
201        },
202        // ignore check to skip buck related code paths, meta (fb) derived starlark, etc.
203        |_source: &LintSource, path: &Utf8Path| -> bool {
204            path.starts_with("buck/") || path.starts_with("third-party/")
205        },
206    ];
207
208    // TODO: handle skipped results
209    let mut errs = false;
210    for (source, message) in &results.messages {
211        if let LintKind::Content(path) = source.kind()
212            && ignore_funcs.iter().any(|func| func(source, path))
213        {
214            continue;
215        }
216        println!(
217            "[{}] [{}] [{}]: {}\n",
218            message.level(),
219            source.name(),
220            source.kind(),
221            message.message()
222        );
223        errs = true;
224    }
225
226    if errs {
227        Err(anyhow!("there were lint errors"))
228    } else {
229        Ok(())
230    }
231}