-
Notifications
You must be signed in to change notification settings - Fork 2.6k
feat: Add a basic linting system #13621
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,10 +24,12 @@ use crate::sources::{PathSource, CRATES_IO_INDEX, CRATES_IO_REGISTRY}; | |
use crate::util::edit_distance; | ||
use crate::util::errors::{CargoResult, ManifestError}; | ||
use crate::util::interning::InternedString; | ||
use crate::util::lints::check_implicit_features; | ||
use crate::util::toml::{read_manifest, InheritableFields}; | ||
use crate::util::{context::ConfigRelativePath, Filesystem, GlobalContext, IntoUrl}; | ||
use cargo_util::paths; | ||
use cargo_util::paths::normalize_path; | ||
use cargo_util_schemas::manifest; | ||
use cargo_util_schemas::manifest::RustVersion; | ||
use cargo_util_schemas::manifest::{TomlDependency, TomlProfiles}; | ||
use pathdiff::diff_paths; | ||
|
@@ -1095,11 +1097,14 @@ impl<'gctx> Workspace<'gctx> { | |
|
||
pub fn emit_warnings(&self) -> CargoResult<()> { | ||
for (path, maybe_pkg) in &self.packages.packages { | ||
let path = path.join("Cargo.toml"); | ||
if let MaybePackage::Package(pkg) = maybe_pkg { | ||
self.emit_lints(pkg, &path)? | ||
} | ||
Comment on lines
+1100
to
+1103
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need a test to ensure this is subject to cap-lints |
||
let warnings = match maybe_pkg { | ||
MaybePackage::Package(pkg) => pkg.manifest().warnings().warnings(), | ||
MaybePackage::Virtual(vm) => vm.warnings().warnings(), | ||
}; | ||
let path = path.join("Cargo.toml"); | ||
for warning in warnings { | ||
if warning.is_critical { | ||
let err = anyhow::format_err!("{}", warning.message); | ||
|
@@ -1121,6 +1126,30 @@ impl<'gctx> Workspace<'gctx> { | |
Ok(()) | ||
} | ||
|
||
pub fn emit_lints(&self, pkg: &Package, path: &Path) -> CargoResult<()> { | ||
let mut error_count = 0; | ||
let lints = pkg | ||
.manifest() | ||
.resolved_toml() | ||
.lints | ||
.clone() | ||
.map(|lints| lints.lints) | ||
.unwrap_or(manifest::TomlLints::default()) | ||
.get("cargo") | ||
.cloned() | ||
.unwrap_or(manifest::TomlToolLints::default()); | ||
|
||
check_implicit_features(pkg, &path, &lints, &mut error_count, self.gctx)?; | ||
weihanglo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if error_count > 0 { | ||
Err(crate::util::errors::AlreadyPrintedError::new(anyhow!( | ||
"encountered {error_count} errors(s) while running lints" | ||
)) | ||
.into()) | ||
} else { | ||
Ok(()) | ||
} | ||
} | ||
|
||
pub fn set_target_dir(&mut self, target_dir: Filesystem) { | ||
self.target_dir = Some(target_dir); | ||
} | ||
|
weihanglo marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
use crate::core::FeatureValue::Dep; | ||
use crate::core::{Edition, FeatureValue, Package}; | ||
use crate::util::interning::InternedString; | ||
use crate::{CargoResult, GlobalContext}; | ||
use annotate_snippets::{Level, Renderer, Snippet}; | ||
use cargo_util_schemas::manifest::{TomlLintLevel, TomlToolLints}; | ||
use pathdiff::diff_paths; | ||
use std::collections::HashSet; | ||
use std::ops::Range; | ||
use std::path::Path; | ||
use toml_edit::ImDocument; | ||
|
||
fn get_span(document: &ImDocument<String>, path: &[&str], get_value: bool) -> Option<Range<usize>> { | ||
weihanglo marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: please put the focus of the file (what comes first) on the API and not the implementation details There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These needs documentation, especially around how arrays are handled |
||
let mut table = document.as_item().as_table_like().unwrap(); | ||
let mut iter = path.into_iter().peekable(); | ||
while let Some(key) = iter.next() { | ||
let (key, item) = table.get_key_value(key).unwrap(); | ||
if iter.peek().is_none() { | ||
return if get_value { | ||
item.span() | ||
} else { | ||
let leaf_decor = key.dotted_decor(); | ||
let leaf_prefix_span = leaf_decor.prefix().and_then(|p| p.span()); | ||
let leaf_suffix_span = leaf_decor.suffix().and_then(|s| s.span()); | ||
if let (Some(leaf_prefix_span), Some(leaf_suffix_span)) = | ||
(leaf_prefix_span, leaf_suffix_span) | ||
{ | ||
Some(leaf_prefix_span.start..leaf_suffix_span.end) | ||
} else { | ||
key.span() | ||
} | ||
}; | ||
} | ||
if item.is_table_like() { | ||
table = item.as_table_like().unwrap(); | ||
} | ||
if item.is_array() && iter.peek().is_some() { | ||
let array = item.as_array().unwrap(); | ||
let next = iter.next().unwrap(); | ||
return array.iter().find_map(|item| { | ||
if next == &item.to_string() { | ||
item.span() | ||
} else { | ||
None | ||
} | ||
}); | ||
} | ||
} | ||
None | ||
} | ||
|
||
/// Gets the relative path to a manifest from the current working directory, or | ||
/// the absolute path of the manifest if a relative path cannot be constructed | ||
fn rel_cwd_manifest_path(path: &Path, gctx: &GlobalContext) -> String { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto, this belongs at the end of the file There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Or should we try to switch to |
||
diff_paths(path, gctx.cwd()) | ||
.unwrap_or_else(|| path.to_path_buf()) | ||
.display() | ||
.to_string() | ||
} | ||
|
||
#[derive(Copy, Clone, Debug)] | ||
pub struct LintGroup { | ||
pub name: &'static str, | ||
pub default_level: LintLevel, | ||
pub desc: &'static str, | ||
pub edition_lint_opts: Option<(Edition, LintLevel)>, | ||
} | ||
|
||
const RUST_2024_COMPATIBILITY: LintGroup = LintGroup { | ||
name: "rust-2024-compatibility", | ||
default_level: LintLevel::Allow, | ||
desc: "warn about compatibility with Rust 2024", | ||
edition_lint_opts: Some((Edition::Edition2024, LintLevel::Deny)), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're only making this |
||
}; | ||
|
||
#[derive(Copy, Clone, Debug)] | ||
pub struct Lint { | ||
pub name: &'static str, | ||
pub desc: &'static str, | ||
pub groups: &'static [LintGroup], | ||
pub default_level: LintLevel, | ||
pub edition_lint_opts: Option<(Edition, LintLevel)>, | ||
} | ||
|
||
impl Lint { | ||
pub fn level(&self, lints: &TomlToolLints, edition: Edition) -> LintLevel { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One thing worth thinking hard is that how to avoid future build failure if people set something like A possible way is having every new lint capped max to warning in the current edition, and relax in the next. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One way to potentially do this is to have each lint have a
I don't think this is perfect but it would work There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think rustc or clippy maintain compatibility with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should verify this handles |
||
let level = self | ||
.groups | ||
.iter() | ||
.map(|g| g.name) | ||
.chain(std::iter::once(self.name)) | ||
.filter_map(|n| lints.get(n).map(|l| (n, l))) | ||
.max_by_key(|(n, l)| (l.priority(), std::cmp::Reverse(*n))); | ||
|
||
match level { | ||
Some((_, toml_lint)) => toml_lint.level().into(), | ||
None => { | ||
if let Some((lint_edition, lint_level)) = self.edition_lint_opts { | ||
if edition >= lint_edition { | ||
return lint_level; | ||
} | ||
} | ||
self.default_level | ||
} | ||
} | ||
} | ||
Comment on lines
+87
to
+106
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we document anywhere the status of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: even if we don't support the group in |
||
} | ||
|
||
#[derive(Copy, Clone, Debug, PartialEq)] | ||
pub enum LintLevel { | ||
Muscraft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Allow, | ||
Warn, | ||
Deny, | ||
Forbid, | ||
} | ||
|
||
impl LintLevel { | ||
pub fn to_diagnostic_level(self) -> Level { | ||
match self { | ||
LintLevel::Allow => Level::Note, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is |
||
LintLevel::Warn => Level::Warning, | ||
LintLevel::Deny => Level::Error, | ||
LintLevel::Forbid => Level::Error, | ||
} | ||
} | ||
} | ||
|
||
impl From<TomlLintLevel> for LintLevel { | ||
fn from(toml_lint_level: TomlLintLevel) -> LintLevel { | ||
match toml_lint_level { | ||
TomlLintLevel::Allow => LintLevel::Allow, | ||
TomlLintLevel::Warn => LintLevel::Warn, | ||
TomlLintLevel::Deny => LintLevel::Deny, | ||
TomlLintLevel::Forbid => LintLevel::Forbid, | ||
} | ||
} | ||
} | ||
|
||
/// By default, cargo will treat any optional dependency as a [feature]. As of | ||
/// cargo 1.60, these can be disabled by declaring a feature that activates the | ||
/// optional dependency as `dep:<name>` (see [RFC #3143]). | ||
/// | ||
/// In the 2024 edition, `cargo` will stop exposing optional dependencies as | ||
/// features implicitly, requiring users to add `foo = ["dep:foo"]` if they | ||
/// still want it exposed. | ||
/// | ||
/// For more information, see [RFC #3491] | ||
/// | ||
/// [feature]: https://doc.rust-lang.org/cargo/reference/features.html | ||
/// [RFC #3143]: https://rust-lang.github.io/rfcs/3143-cargo-weak-namespaced-features.html | ||
/// [RFC #3491]: https://rust-lang.github.io/rfcs/3491-remove-implicit-features.html | ||
const IMPLICIT_FEATURES: Lint = Lint { | ||
weihanglo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
name: "implicit-features", | ||
desc: "warn about the use of unstable features", | ||
groups: &[RUST_2024_COMPATIBILITY], | ||
default_level: LintLevel::Allow, | ||
edition_lint_opts: Some((Edition::Edition2024, LintLevel::Deny)), | ||
}; | ||
|
||
pub fn check_implicit_features( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. imo its a bit messy to shove lint implementations in here just because they are lints. This body should live where its called. |
||
pkg: &Package, | ||
path: &Path, | ||
lints: &TomlToolLints, | ||
error_count: &mut usize, | ||
gctx: &GlobalContext, | ||
) -> CargoResult<()> { | ||
let lint_level = IMPLICIT_FEATURES.level(lints, pkg.manifest().edition()); | ||
if lint_level == LintLevel::Allow { | ||
return Ok(()); | ||
} | ||
|
||
let manifest = pkg.manifest(); | ||
let user_defined_features = manifest.resolved_toml().features(); | ||
let features = user_defined_features.map_or(HashSet::new(), |f| { | ||
f.keys().map(|k| InternedString::new(&k)).collect() | ||
}); | ||
// Add implicit features for optional dependencies if they weren't | ||
// explicitly listed anywhere. | ||
let explicitly_listed = user_defined_features.map_or(HashSet::new(), |f| { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We might want some more test cases to exercising implicit features lint.
Since this PR aims for the linting system. Let's leave it later. |
||
f.values() | ||
.flatten() | ||
.filter_map(|v| match FeatureValue::new(v.into()) { | ||
Dep { dep_name } => Some(dep_name), | ||
_ => None, | ||
}) | ||
.collect() | ||
}); | ||
|
||
for dep in manifest.dependencies() { | ||
let dep_name_in_toml = dep.name_in_toml(); | ||
if !dep.is_optional() | ||
|| features.contains(&dep_name_in_toml) | ||
|| explicitly_listed.contains(&dep_name_in_toml) | ||
{ | ||
continue; | ||
} | ||
if lint_level == LintLevel::Forbid || lint_level == LintLevel::Deny { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we have an |
||
*error_count += 1; | ||
} | ||
let level = lint_level.to_diagnostic_level(); | ||
let manifest_path = rel_cwd_manifest_path(path, gctx); | ||
let message = level.title("unused optional dependency").snippet( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This message doesn't make sense pre-2024 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we have a transitional note that implicit features are no longer supported? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rustc/clippy lints display the lint name (and where the level was set) on first instance of it being reported |
||
Snippet::source(manifest.contents()) | ||
.origin(&manifest_path) | ||
.annotation( | ||
level.span( | ||
get_span( | ||
manifest.document(), | ||
&["dependencies", &dep_name_in_toml], | ||
false, | ||
) | ||
.unwrap(), | ||
), | ||
) | ||
.fold(true), | ||
); | ||
let renderer = Renderer::styled().term_width( | ||
gctx.shell() | ||
.err_width() | ||
.diagnostic_terminal_width() | ||
.unwrap_or(annotate_snippets::renderer::DEFAULT_TERM_WIDTH), | ||
); | ||
writeln!(gctx.shell().err(), "{}", renderer.render(message))?; | ||
} | ||
Ok(()) | ||
} |
Uh oh!
There was an error while loading. Please reload this page.