use crate::code_lint::ast::ParsedFile;
use crate::code_lint::contract::{CodeRule, RuleTarget};
use crate::diagnostic::{Diagnostic, Language, RuleName, ViolationTemplate, violation_template};
use crate::rule_declaration::{
Classification, Consensus, Declaration, Example, FilterListDefaults, ImpactedQuality, ListKind,
ListOption, Precision, Reference, RuleDoc, RuleOptions, Topic,
};
use std::collections::HashSet;
use std::path::Path;
const BANNED: ListOption = ListOption {
kind: ListKind::Deny,
doc: "Abbreviations flagged as a word of an identifier.",
default: FilterListDefaults {
base: &[
"err", "ctx", "cfg", "res", "msg", "num", "btn", "cb", "ch", "diag", "ty", "cat",
"stmt", "ext", "fmt", "arch", "vis",
],
extend: &[],
remove: &[],
},
};
const TEMPLATE: ViolationTemplate = violation_template! {
summary: "Name `{name}` contains the abbreviation `{token}`.",
rationale: "An ambiguous shorthand forces the reader to guess the word it stands for and fragments searches across the codebase.",
suggestion: "Rename `{name}` to spell out `{token}` in full.",
};
pub const RULE: CodeRule<ListOption> = CodeRule {
declaration: Declaration {
name: RuleName("abbreviated-name"),
template: &TEMPLATE,
languages: &[Language::Python, Language::Rust],
options: RuleOptions::code_rule(BANNED),
classification: Classification {
topics: &[Topic::ABBREVIATED_NAMES],
precision: Precision::Heuristic,
consensus: Consensus::Opinionated,
impacted_quality: ImpactedQuality::Maintainability,
},
doc: RuleDoc {
summary: "Flags names that contain an abbreviation such as `ctx` or `msg`.",
what_it_does: indoc::indoc! {r"
Flags a name the code defines when one of its words is a banned abbreviation,
such as `handle_msg` or `TaskRes`. Only whole words match, so `strategy` and
`category` are not flagged. Import aliases such as `import numpy as np`,
abbreviations inherited from an imported symbol in the file (such as `ext_bot`
when `ExtBot` is imported), and names imposed by a contract (Python `@override`
methods, members of a Rust `impl Trait for Type` block) are skipped, but the
parameters of those methods are still checked."},
why_is_this_bad: indoc::indoc! {r"
An abbreviation makes the reader guess: `res` can be a result, a response or a
resource, `ch` a channel or a character. Different authors also shorten the same
word differently (`cfg`, `conf`, `config`), so a search for one spelling misses the
others.
Spell the word out (`context`, `message`, `result`, `config`)."},
known_problems: None,
references: &[Reference {
title: "Google Python Style Guide: Naming",
url: "https://google.github.io/styleguide/pyguide.html#316-naming",
}],
examples: &[
Example {
language: Language::Python,
flagged: indoc::indoc! {r"
def send_alert(notifier, msg):
notifier.publish(msg)
"},
flagged_span: "msg",
fixed: indoc::indoc! {r"
def send_alert(notifier, message):
notifier.publish(message)
"},
},
Example {
language: Language::Rust,
flagged: indoc::indoc! {r"
fn send_alert(notifier: &Notifier, msg: &str) {
notifier.publish(msg);
}
"},
flagged_span: "msg",
fixed: indoc::indoc! {r"
fn send_alert(notifier: &Notifier, message: &str) {
notifier.publish(message);
}
"},
},
],
},
},
target: RuleTarget::All,
check: check_file,
};
fn check_file(
rule: &CodeRule<ListOption>,
path: &Path,
file: &ParsedFile,
banned: &HashSet<String>,
) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
let imported_segments: Vec<Vec<String>> =
crate::code_lint::semantic::bindings::collect_imported_names(file)
.into_iter()
.map(|name| split_segments(&name))
.filter(|segments| !segments.is_empty())
.collect();
for node in crate::code_lint::semantic::bindings::collect_renameable_bindings(file) {
let name = node.text();
let segments = split_segments(&name);
for segment in non_imported_segments(&segments, &imported_segments) {
if banned.contains(segment) {
diagnostics.push(rule.diagnostic_at_node(
path,
&node,
&[("name", &name), ("token", segment)],
));
break;
}
}
}
diagnostics
}
fn non_imported_segments<'a>(
segments: &'a [String],
imported_segments: &[Vec<String>],
) -> Vec<&'a str> {
segments
.iter()
.enumerate()
.filter(|&(index, _)| {
!imported_segments.iter().any(|imported| {
!imported.is_empty()
&& segments
.windows(imported.len())
.enumerate()
.any(|(start, window)| {
window == imported.as_slice()
&& index >= start
&& index < start + imported.len()
})
})
})
.map(|(_, segment)| segment.as_str())
.collect()
}
fn split_segments(name: &str) -> Vec<String> {
let mut segments = Vec::new();
let mut current = String::new();
let mut previous_char: Option<char> = None;
for character in name.chars() {
if character == '_' {
if !current.is_empty() {
segments.push(current.to_lowercase());
current.clear();
}
previous_char = None;
continue;
}
if let Some(prev) = previous_char
&& character.is_uppercase()
&& (prev.is_lowercase() || prev.is_numeric())
&& !current.is_empty()
{
segments.push(current.to_lowercase());
current.clear();
}
current.push(character);
previous_char = Some(character);
}
if !current.is_empty() {
segments.push(current.to_lowercase());
}
segments
}
#[cfg(test)]
crate::test_utils::rule_test!(
RULE,
{
Python => {
pass: [
full_domain_words_allowed => r#"
def handle_message(context, error_message):
manager = "active"
configuration = 42
"#,
substring_containing_banned_token_allowed => r#"
def process(strategy, category):
pass
"#,
unaliased_import_statement_exempt => r#"
import cfg
"#,
unaliased_from_import_exempt => r#"
from os import error_msg
"#,
aliased_import_exempt => r#"
import os as os_cfg
"#,
imported_symbol_abbreviation_exempt => r#"
from telegram.ext import ExtBot
def notify(ext_bot: ExtBot, primary_ext_bot: ExtBot) -> None:
active_ext_bot = ext_bot
"#,
override_method_contract_exempt => r#"
from typing import override
class CustomDecoder(BaseDecoder):
@override
def from_ctx(self, data: bytes) -> None:
pass
"#,
attribute_writes_outside_declarations_not_checked => r#"
class Worker:
def update(self, items, index) -> None:
self.ctx = 1
items[index] = 2
"#,
],
fail: [
camel_case_class_name => r#"
class TaskRes:
pass
"# => "TaskRes",
snake_case_function_name => r#"
def handle_msg():
pass
"# => "handle_msg",
parameter_name => r#"
def process(req_ctx):
pass
"# => "req_ctx",
typed_varargs_parameter_name => r#"
def process(*req_ctx: int):
pass
"# => "req_ctx",
digit_to_uppercase_split => r#"
def process(v2Ctx):
pass
"# => "v2Ctx",
single_diagnostic_when_multiple_tokens_banned => r#"
err_msg = "failed"
"# => "err_msg",
unrelated_abbreviation_not_exempted_by_imported_symbol => r#"
from telegram.ext import ExtBot
def notify(ext_bot: ExtBot, file_ext: str) -> None:
pass
"# => "file_ext",
unannotated_method_flagged => r#"
class CustomDecoder:
def from_ctx(self, data: bytes) -> None:
pass
"# => "from_ctx",
init_attribute_declaration => r#"
class Worker:
def __init__(self, context) -> None:
self.ctx = context
self.ctx = None
"# => "ctx",
class_body_attribute_declaration => r#"
class Worker:
ctx: str
def __init__(self, context: str) -> None:
self.ctx = context
"# => "ctx",
],
},
Rust => {
pass: [
full_domain_words_allowed => r#"
fn handle_context(error_message: &str) {
let configuration = 1;
}
"#,
substring_containing_banned_token_allowed => r#"
fn process(strategy: usize, category: usize) {}
"#,
unaliased_imports_exempt => r#"
use std::fmt;
"#,
aliased_import_exempt => r#"
use std::collections::HashMap as my_cfg;
"#,
imported_symbol_abbreviation_exempt => r#"
use telegram::ExtBot;
fn notify(ext_bot: &ExtBot, primary_ext_bot: &ExtBot) {
let active_ext_bot = ext_bot;
}
"#,
trait_impl_associated_type_exempt => r#"
impl Decoder for Wrapper {
type Err = ();
}
"#,
trait_impl_method_exempt => r#"
impl Decoder for Wrapper {
fn from_ctx(&self) {}
}
"#,
],
fail: [
camel_case_struct_name => r#"
struct MyRes;
"# => "MyRes",
snake_case_function_name => r#"
fn process_err() {}
"# => "process_err",
digit_to_uppercase_split => r#"
fn main() {
let v2Ctx = 2;
}
"# => "v2Ctx",
single_diagnostic_when_multiple_tokens_banned => r#"
fn main() {
let err_msg = 1;
}
"# => "err_msg",
unrelated_abbreviation_not_exempted_by_imported_symbol => r#"
use telegram::ExtBot;
fn notify(ext_bot: &ExtBot, file_ext: &str) {}
"# => "file_ext",
trait_impl_parameter_flagged => r#"
impl Decoder for Wrapper {
fn from_ctx(&self, msg: u8) {}
}
"# => "msg",
inherent_method_flagged => r#"
impl Wrapper {
fn from_ctx(&self) {}
}
"# => "from_ctx",
struct_field => r#"
struct Request {
ctx: Context,
body: String,
}
"# => "ctx",
],
},
}
);