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        // This one seems to be broken
110        // &UnpublishedPackagesOnlyUsePathDependencies::new(),
111        &PublishedPackagesDontDependOnUnpublishedPackages,
112        &OnlyPublishToCratesIo,
113        &CratesInCratesDirectory,
114        // There are crates under consensus/, external-crates/.
115        // &CratesOnlyInCratesDirectory,
116    ];
117
118    let file_path_linters: &[&dyn FilePathLinter] = &[
119        // &AllowedPaths::new(DEFAULT_ALLOWED_PATHS_REGEX)?
120        ];
121
122    // allow whitespace exceptions for markdown files
123    // let whitespace_exceptions = build_exceptions(&["*.md".to_owned()])?;
124    let content_linters: &[&dyn ContentLinter] = &[
125        &LicenseHeader::new(LICENSE_HEADER),
126        &RootToml,
127        // &EofNewline::new(&whitespace_exceptions),
128        // &TrailingWhitespace::new(&whitespace_exceptions),
129    ];
130
131    let nexlint_context = NexLintContext::from_current_dir()?;
132    let engine = LintEngineConfig::new(&nexlint_context)
133        .with_project_linters(project_linters)
134        .with_package_linters(package_linters)
135        .with_file_path_linters(file_path_linters)
136        .with_content_linters(content_linters)
137        .fail_fast(args.fail_fast)
138        .build();
139
140    let results = engine.run()?;
141
142    handle_lint_results_exclude_external_crate_checks(results)
143}
144
145/// Define custom handler so we can skip certain lints on certain files. This is a temporary till we upstream this logic
146pub fn handle_lint_results_exclude_external_crate_checks(
147    results: LintResults,
148) -> crate::Result<()> {
149    // ignore_funcs is a slice of funcs to execute against lint sources and their path
150    // if a func returns true, it means it will be ignored and not throw a lint error
151    let ignore_funcs = [
152        // legacy ignore checks
153        |source: &LintSource, path: &Utf8Path| -> bool {
154            (path.starts_with(EXTERNAL_CRATE_DIR)
155                || path.starts_with(CREATE_DAPP_TEMPLATE_DIR)
156                || path.to_string().contains("/generated/")
157                || path.to_string().contains("/proto/"))
158                && source.name() == "license-header"
159        },
160        // ignore check to skip buck related code paths, meta (fb) derived starlark, etc.
161        |_source: &LintSource, path: &Utf8Path| -> bool {
162            path.starts_with("buck/") || path.starts_with("third-party/")
163        },
164    ];
165
166    // TODO: handle skipped results
167    let mut errs = false;
168    for (source, message) in &results.messages {
169        if let LintKind::Content(path) = source.kind()
170            && ignore_funcs.iter().any(|func| func(source, path))
171        {
172            continue;
173        }
174        println!(
175            "[{}] [{}] [{}]: {}\n",
176            message.level(),
177            source.name(),
178            source.kind(),
179            message.message()
180        );
181        errs = true;
182    }
183
184    if errs {
185        Err(anyhow!("there were lint errors"))
186    } else {
187        Ok(())
188    }
189}