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