Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f58ab25
Add version, file, and line metadata to `Violation` structs
ntBre Oct 22, 2025
6718d87
[WIP] initial attempt with claude
ntBre Oct 15, 2025
74016c0
update ViolationMetadata derive with violation_metadata attribute
ntBre Oct 22, 2025
6718eb9
add file and line information while we're at it
ntBre Oct 22, 2025
3270346
cleaning up rule script
ntBre Oct 22, 2025
ccacc8b
further script improvements and much better results
ntBre Oct 22, 2025
1d68663
script for actually writing the metadata
ntBre Oct 22, 2025
c4f7fae
wire up metadata methods to Rule
ntBre Oct 22, 2025
c46899d
include metadata in generated docs
ntBre Oct 22, 2025
def4d33
delete totally unused scripts from claude
ntBre Oct 22, 2025
9d65b86
include RuleGroup in added metadata
ntBre Oct 23, 2025
a73876e
move group to metadata macros
ntBre Oct 23, 2025
c0e6545
switch back from String
ntBre Oct 23, 2025
08f4aa0
update add_rule.py
ntBre Oct 23, 2025
3904813
improve rule issue link
ntBre Oct 23, 2025
90e4fcf
update docs with rule group
ntBre Oct 23, 2025
329edc6
combine version with RuleGroup
ntBre Oct 23, 2025
9adea4c
add SourceLocation to `ruff rule` JSON output
ntBre Oct 23, 2025
c28ef67
filter file path to avoid path separator issues
ntBre Oct 23, 2025
08f4215
update get_nested_attrs name and add some docs
ntBre Oct 23, 2025
b0248ae
remove RuleGroup from map_codes calls
ntBre Oct 23, 2025
32d8f45
add metadata to rules
ntBre Oct 22, 2025
b1be096
ignore doctest
ntBre Oct 23, 2025
e1ee1ad
delete helper scripts and csv file
ntBre Oct 23, 2025
34ec0fc
update removed rule versions
ntBre Oct 23, 2025
026e775
update stable rule versions
ntBre Oct 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions add_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from __future__ import annotations

import csv
import glob
import json
import re
import subprocess
from pathlib import Path

Rule = str
Version = str


def kebab_to_pascal(name):
return "".join(word.capitalize() for word in name.split("-"))


def load_rules() -> dict[Rule, str]:
rules = json.loads(
subprocess.run(
["ruff", "rule", "--all", "--output-format", "json"],
capture_output=True,
check=True,
).stdout
)

return {rule["code"]: kebab_to_pascal(rule["name"]) for rule in rules}


def load_versions() -> dict[Rule, Version]:
versions = {}
with open("ruff_rules_metadata.csv") as f:
reader = csv.reader(f)
for i, line in enumerate(reader):
if i == 0:
continue
rule, version, _ = line
versions[rule] = version

return versions


if __name__ == "__main__":
rules = load_rules()
versions = load_versions()

for rule, name in rules.items():
print(f"searching for {rule}")
pattern = re.compile(rf"pub(\(crate\))? struct {name}( [{{]|;|<|\()", re.I)
for path in glob.glob("crates/ruff_linter/src/rules/**/*.rs", recursive=True):
path = Path(path)
contents = path.read_text()
if pattern.search(contents):
new_contents = pattern.sub(
rf'#[violation_metadata(version = "{versions[rule]}")]\n\g<0>',
contents,
)
path.write_text(new_contents)
break
else:
raise ValueError(f"failed to locate definition for {name}")
19 changes: 19 additions & 0 deletions crates/ruff_dev/src/generate_docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,25 @@ pub(crate) fn main(args: &Args) -> Result<()> {

let _ = writeln!(&mut output, "# {} ({})", rule.name(), rule.noqa_code());

let _ = writeln!(
&mut output,
r#"<small>
Added in <a href="https://github.com/astral-sh/ruff/releases/tag/{version}">{version}</a> ·
<a href="https://github.com/astral-sh/ruff/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20{encoded_name}" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/{file}#L{line}" target="_blank">View source</a>
</small>

"#,
version = rule.version().unwrap_or(env!("CARGO_PKG_VERSION")),
encoded_name =
url::form_urlencoded::byte_serialize(rule.name().as_str().as_bytes())
.collect::<String>(),
file =
url::form_urlencoded::byte_serialize(rule.file().replace('\\', "/").as_bytes())
.collect::<String>(),
line = rule.line(),
);

let (linter, _) = Linter::parse_code(&rule.noqa_code().to_string()).unwrap();
if linter.url().is_some() {
let common_prefix: String = match linter.common_prefix() {
Expand Down
9 changes: 9 additions & 0 deletions crates/ruff_linter/src/violation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ pub trait ViolationMetadata {
/// Returns an explanation of what this violation catches,
/// why it's bad, and what users should do instead.
fn explain() -> Option<&'static str>;

/// The Ruff version when a rule was first introduced, e.g. 0.1.2.
fn version() -> Option<&'static str>;

/// The file where the violation is declared.
fn file() -> &'static str;

/// The 1-based line where the violation is declared.
fn line() -> u32;
}

pub trait Violation: ViolationMetadata + Sized {
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ pub fn cache_key(input: TokenStream) -> TokenStream {
TokenStream::from(stream)
}

#[proc_macro_derive(ViolationMetadata)]
#[proc_macro_derive(ViolationMetadata, attributes(violation_metadata))]
pub fn derive_violation_metadata(item: TokenStream) -> TokenStream {
let input: DeriveInput = parse_macro_input!(item);

Expand Down
24 changes: 24 additions & 0 deletions crates/ruff_macros/src/map_codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,9 @@ fn register_rules<'a>(input: impl Iterator<Item = &'a Rule>) -> TokenStream {
let mut rule_message_formats_match_arms = quote!();
let mut rule_fixable_match_arms = quote!();
let mut rule_explanation_match_arms = quote!();
let mut rule_version_match_arms = quote!();
let mut rule_file_match_arms = quote!();
let mut rule_line_match_arms = quote!();

for Rule {
name, attrs, path, ..
Expand All @@ -420,6 +423,15 @@ fn register_rules<'a>(input: impl Iterator<Item = &'a Rule>) -> TokenStream {
quote! {#(#attrs)* Self::#name => <#path as crate::Violation>::FIX_AVAILABILITY,},
);
rule_explanation_match_arms.extend(quote! {#(#attrs)* Self::#name => #path::explain(),});
rule_version_match_arms.extend(
quote! {#(#attrs)* Self::#name => <#path as crate::ViolationMetadata>::version(),},
);
rule_file_match_arms.extend(
quote! {#(#attrs)* Self::#name => <#path as crate::ViolationMetadata>::file(),},
);
rule_line_match_arms.extend(
quote! {#(#attrs)* Self::#name => <#path as crate::ViolationMetadata>::line(),},
);
}

quote! {
Expand Down Expand Up @@ -455,6 +467,18 @@ fn register_rules<'a>(input: impl Iterator<Item = &'a Rule>) -> TokenStream {
pub const fn fixable(&self) -> crate::FixAvailability {
match self { #rule_fixable_match_arms }
}

pub fn version(&self) -> Option<&'static str> {
match self { #rule_version_match_arms }
}

pub fn file(&self) -> &'static str {
match self { #rule_file_match_arms }
}

pub fn line(&self) -> u32 {
match self { #rule_line_match_arms }
}
}
}
}
Expand Down
38 changes: 38 additions & 0 deletions crates/ruff_macros/src/violation_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ use syn::{Attribute, DeriveInput, Error, Lit, LitStr, Meta};
pub(crate) fn violation_metadata(input: DeriveInput) -> syn::Result<TokenStream> {
let docs = get_docs(&input.attrs)?;

let version = match get_version(&input.attrs)? {
Some(version) => quote!(Some(#version)),
None => quote!(None),
};

let name = input.ident;

let (impl_generics, ty_generics, where_clause) = &input.generics.split_for_impl();
Expand All @@ -20,6 +25,18 @@ pub(crate) fn violation_metadata(input: DeriveInput) -> syn::Result<TokenStream>
fn explain() -> Option<&'static str> {
Some(#docs)
}

fn version() -> Option<&'static str> {
#version
}

fn file() -> &'static str {
file!()
}

fn line() -> u32 {
line!()
}
}
})
}
Expand All @@ -43,6 +60,27 @@ fn get_docs(attrs: &[Attribute]) -> syn::Result<String> {
Ok(explanation)
}

/// Extract the version attribute as a string.
fn get_version(attrs: &[Attribute]) -> syn::Result<Option<String>> {
let mut version = None;
for attr in attrs {
if attr.path().is_ident("violation_metadata") {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("version") {
let lit: LitStr = meta.value()?.parse()?;
version = Some(lit.value());
return Ok(());
}
Err(Error::new_spanned(
attr,
"unimplemented violation metadata option",
))
})?;
}
}
Ok(version)
}

fn parse_attr<'a, const LEN: usize>(
path: [&'static str; LEN],
attr: &'a Attribute,
Expand Down
Loading