1use 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 "serde_yaml".to_owned(),
81 "syn".to_owned(),
82 "prost".to_owned(),
85 "tonic".to_owned(),
86 "http-body".to_owned(),
88 "tower".to_owned(),
90 "axum".to_owned(),
92 "axum-extra".to_owned(),
93 "bincode".to_owned(),
95 "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 &PublishedPackagesDontDependOnUnpublishedPackages,
113 &OnlyPublishToCratesIo,
114 &CratesInCratesDirectory,
115 ];
118
119 let file_path_linters: &[&dyn FilePathLinter] = &[
120 ];
122
123 let content_linters: &[&dyn ContentLinter] = &[
126 &LicenseHeader::new(LICENSE_HEADER),
127 &RootToml,
128 ];
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#[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
186pub fn handle_lint_results_exclude_external_crate_checks(
188 results: LintResults,
189) -> crate::Result<()> {
190 let ignore_funcs = [
193 |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 |_source: &LintSource, path: &Utf8Path| -> bool {
204 path.starts_with("buck/") || path.starts_with("third-party/")
205 },
206 ];
207
208 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}