use regex::Regex;
use once_cell::sync::Lazy;
use crate::cop::{Category, Cop, Severity};
use crate::offense::{Location, Offense};
use crate::source::SourceFile;
static AMBIGUOUS_ASSIGNMENT: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"\b([a-z_][a-z0-9_]*)\s*=([+\-*/])\s"#).unwrap()
});
static _ASSIGNMENT_IN_CONDITION: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"\b(if|unless|while|until)\s+.*?([a-z_][a-z0-9_]*)\s*=[^=]"#).unwrap()
});
static BIGDECIMAL_NEW: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"\bBigDecimal\.new\b"#).unwrap()
});
static BINARY_IDENTICAL_OPERANDS: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"\b([a-z_][a-z0-9_]*)\s*(==|!=|<|>|<=|>=|\|\||&&)\s*([a-z_][a-z0-9_]*)\b"#).unwrap()
});
static BOOLEAN_SYMBOL: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#":(true|false)\b"#).unwrap()
});
static _CONSTANT_DEFINITION_IN_BLOCK: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"\b(do|each|map|select)\s+.*?[A-Z][A-Z0-9_]*\s*="#).unwrap()
});
static DEPRECATED_CLASS_METHODS: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"(File\.exists\?|Dir\.exists\?)"#).unwrap()
});
static _DUPLICATE_HASH_KEY: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"([a-z_][a-z0-9_]*):"#).unwrap()
});
static EMPTY_BLOCK: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"\{\s*\}|\bdo\s*\bend"#).unwrap()
});
static _EMPTY_CLASS: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"^\s*class\s+[A-Z][a-zA-Z0-9]*\s*\n\s*end"#).unwrap()
});
static EMPTY_INTERPOLATION: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"#\{\s*\}"#).unwrap()
});
static _ENSURE_RETURN: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"\bensure\b"#).unwrap()
});
static FLOAT_COMPARISON: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"([a-z_][a-z0-9_]*|[\d.]+)\s*==\s*([a-z_][a-z0-9_]*|[\d.]+)"#).unwrap()
});
static LOOP_WHILE_TRUE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"\b(while\s+true|until\s+false)\b"#).unwrap()
});
static _NESTED_METHOD_DEF: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"^\s*def\s+"#).unwrap()
});
static RAND_ONE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"\brand\(1\)"#).unwrap()
});
static REDUNDANT_STRING_COERCION: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"#\{([a-z_][a-z0-9_]*)\.to_s\}"#).unwrap()
});
static RESCUE_EXCEPTION: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"\brescue\s+(Exception|StandardError)\b"#).unwrap()
});
static SELF_ASSIGNMENT: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"\b([a-z_][a-z0-9_]*)\s*=\s*([a-z_][a-z0-9_]*)\b"#).unwrap()
});
static _SUPPRESSED_EXCEPTION: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"\brescue\s*\n"#).unwrap()
});
static _UNREACHABLE_CODE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"\b(return|raise|break|next)\b"#).unwrap()
});
static _USELESS_ASSIGNMENT: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"\b([a-z_][a-z0-9_]*)\s*="#).unwrap()
});
static _VOID_CONTEXT: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"^\s*(&&|\|\||<=>|==)"#).unwrap()
});
pub struct AmbiguousAssignment;
impl Cop for AmbiguousAssignment {
fn name(&self) -> &str { "Lint/AmbiguousAssignment" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str {
"Checks for ambiguous operators that need clarification"
}
fn check(&self, source: &SourceFile) -> Vec<Offense> {
let mut offenses = Vec::new();
for (line_num, line) in source.lines.iter().enumerate() {
let line_number = line_num + 1;
for cap in AMBIGUOUS_ASSIGNMENT.captures_iter(line) {
let full_match = cap.get(0).unwrap();
let column = full_match.start() + 1;
if source.in_string_or_comment(line_number, column) {
continue;
}
offenses.push(Offense::new(
self.name(),
format!("Suspicious assignment '={}'. Did you mean '+='?", cap.get(2).unwrap().as_str()),
self.severity(),
Location::new(line_number, column, full_match.len()),
));
}
}
offenses
}
}
pub struct AmbiguousOperator;
impl Cop for AmbiguousOperator {
fn name(&self) -> &str { "Lint/AmbiguousOperator" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str {
"Checks for ambiguous operators in argument position"
}
fn check(&self, source: &SourceFile) -> Vec<Offense> {
let mut offenses = Vec::new();
let pattern = Regex::new(r#"(\w+)\s+([+\-*/])\s*[\w\d]"#).unwrap();
for (line_num, line) in source.lines.iter().enumerate() {
let line_number = line_num + 1;
for cap in pattern.captures_iter(line) {
let full_match = cap.get(0).unwrap();
let column = full_match.start() + 1;
if source.in_string_or_comment(line_number, column) {
continue;
}
offenses.push(Offense::new(
self.name(),
"Ambiguous operator. Use parentheses for clarity.",
self.severity(),
Location::new(line_number, column, full_match.len()),
));
}
}
offenses
}
}
pub struct AmbiguousRegexpLiteral;
impl Cop for AmbiguousRegexpLiteral {
fn name(&self) -> &str { "Lint/AmbiguousRegexpLiteral" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str {
"Checks for ambiguous regexp literals in method calls"
}
fn check(&self, source: &SourceFile) -> Vec<Offense> {
let mut offenses = Vec::new();
let pattern = Regex::new(r#"(\w+)\s+/[^/]+/"#).unwrap();
for (line_num, line) in source.lines.iter().enumerate() {
let line_number = line_num + 1;
for cap in pattern.captures_iter(line) {
let full_match = cap.get(0).unwrap();
let column = full_match.start() + 1;
if source.in_string_or_comment(line_number, column) {
continue;
}
offenses.push(Offense::new(
self.name(),
"Ambiguous regexp literal. Use parentheses.",
self.severity(),
Location::new(line_number, column, full_match.len()),
));
}
}
offenses
}
}
pub struct AmbiguousRange;
impl Cop for AmbiguousRange {
fn name(&self) -> &str { "Lint/AmbiguousRange" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str {
"Checks for ambiguous range literals"
}
fn check(&self, source: &SourceFile) -> Vec<Offense> {
let mut offenses = Vec::new();
let pattern = Regex::new(r#"(\d+)\.\.\.?-\d+"#).unwrap();
for (line_num, line) in source.lines.iter().enumerate() {
let line_number = line_num + 1;
for cap in pattern.captures_iter(line) {
let full_match = cap.get(0).unwrap();
let column = full_match.start() + 1;
if source.in_string_or_comment(line_number, column) {
continue;
}
offenses.push(Offense::new(
self.name(),
"Ambiguous range literal. Use parentheses.",
self.severity(),
Location::new(line_number, column, full_match.len()),
));
}
}
offenses
}
}
pub struct AssignmentInCondition;
impl Cop for AssignmentInCondition {
fn name(&self) -> &str { "Lint/AssignmentInCondition" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str {
"Checks for assignments in conditions"
}
fn check(&self, source: &SourceFile) -> Vec<Offense> {
let mut offenses = Vec::new();
let pattern = Regex::new(r#"\b(if|unless|while|until)\s+.*="#).unwrap();
for (line_num, line) in source.lines.iter().enumerate() {
let line_number = line_num + 1;
if pattern.is_match(line) && !source.in_string_or_comment(line_number, 1) {
let chars: Vec<char> = line.chars().collect();
for (i, &ch) in chars.iter().enumerate() {
if ch == '=' {
let before = if i > 0 { chars.get(i - 1) } else { None };
let after = chars.get(i + 1);
let is_comparison = matches!(before, Some(&'!') | Some(&'<') | Some(&'>') | Some(&'='))
|| matches!(after, Some(&'=') | Some(&'~'));
if !is_comparison {
offenses.push(Offense::new(
self.name(),
"Assignment in condition - use comparison or wrap in parentheses.",
self.severity(),
Location::new(line_number, i + 1, 1),
));
break;
}
}
}
}
}
offenses
}
}
pub struct BigDecimalNew;
impl Cop for BigDecimalNew {
fn name(&self) -> &str { "Lint/BigDecimalNew" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str {
"Checks for BigDecimal.new usage - use BigDecimal() instead"
}
fn check(&self, source: &SourceFile) -> Vec<Offense> {
let mut offenses = Vec::new();
for (line_num, line) in source.lines.iter().enumerate() {
let line_number = line_num + 1;
for mat in BIGDECIMAL_NEW.find_iter(line) {
let column = mat.start() + 1;
if source.in_string_or_comment(line_number, column) {
continue;
}
offenses.push(Offense::new(
self.name(),
"Use `BigDecimal()` instead of `BigDecimal.new`.",
self.severity(),
Location::new(line_number, column, mat.len()),
));
}
}
offenses
}
}
pub struct BinaryOperatorWithIdenticalOperands;
impl Cop for BinaryOperatorWithIdenticalOperands {
fn name(&self) -> &str { "Lint/BinaryOperatorWithIdenticalOperands" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str {
"Checks for binary operators with identical operands"
}
fn check(&self, source: &SourceFile) -> Vec<Offense> {
let mut offenses = Vec::new();
for (line_num, line) in source.lines.iter().enumerate() {
let line_number = line_num + 1;
for cap in BINARY_IDENTICAL_OPERANDS.captures_iter(line) {
let full_match = cap.get(0).unwrap();
let left_operand = cap.get(1).unwrap().as_str();
let right_operand = cap.get(3).unwrap().as_str();
if left_operand != right_operand {
continue;
}
let column = full_match.start() + 1;
if source.in_string_or_comment(line_number, column) {
continue;
}
offenses.push(Offense::new(
self.name(),
format!("Binary operator with identical operands: `{}`", full_match.as_str()),
self.severity(),
Location::new(line_number, column, full_match.len()),
));
}
}
offenses
}
}
pub struct BooleanSymbol;
impl Cop for BooleanSymbol {
fn name(&self) -> &str { "Lint/BooleanSymbol" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str {
"Checks for :true and :false symbols"
}
fn check(&self, source: &SourceFile) -> Vec<Offense> {
let mut offenses = Vec::new();
for (line_num, line) in source.lines.iter().enumerate() {
let line_number = line_num + 1;
for cap in BOOLEAN_SYMBOL.captures_iter(line) {
let full_match = cap.get(0).unwrap();
let column = full_match.start() + 1;
if source.in_string_or_comment(line_number, column) {
continue;
}
offenses.push(Offense::new(
self.name(),
format!("Avoid using `{}` - use boolean instead.", full_match.as_str()),
self.severity(),
Location::new(line_number, column, full_match.len()),
));
}
}
offenses
}
}
pub struct ConstantDefinitionInBlock;
impl Cop for ConstantDefinitionInBlock {
fn name(&self) -> &str { "Lint/ConstantDefinitionInBlock" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str {
"Checks for constants defined in blocks"
}
fn check(&self, source: &SourceFile) -> Vec<Offense> {
let mut offenses = Vec::new();
let pattern = Regex::new(r#"\b[A-Z][A-Z0-9_]*\s*="#).unwrap();
let mut in_block = false;
for (line_num, line) in source.lines.iter().enumerate() {
let line_number = line_num + 1;
if line.contains(" do") || line.contains(" {") {
in_block = true;
}
if line.contains("end") || line.contains("}") {
in_block = false;
}
if in_block {
for mat in pattern.find_iter(line) {
let column = mat.start() + 1;
if source.in_string_or_comment(line_number, column) {
continue;
}
offenses.push(Offense::new(
self.name(),
"Do not define constants in blocks.",
self.severity(),
Location::new(line_number, column, mat.len()),
));
}
}
}
offenses
}
}
pub struct ConstantReassignment;
impl Cop for ConstantReassignment {
fn name(&self) -> &str { "Lint/ConstantReassignment" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str { "Checks for constant reassignment" }
fn check(&self, _source: &SourceFile) -> Vec<Offense> {
Vec::new()
}
}
pub struct DeprecatedClassMethods;
impl Cop for DeprecatedClassMethods {
fn name(&self) -> &str { "Lint/DeprecatedClassMethods" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str {
"Checks for deprecated class methods"
}
fn check(&self, source: &SourceFile) -> Vec<Offense> {
let mut offenses = Vec::new();
for (line_num, line) in source.lines.iter().enumerate() {
let line_number = line_num + 1;
for cap in DEPRECATED_CLASS_METHODS.captures_iter(line) {
let full_match = cap.get(0).unwrap();
let column = full_match.start() + 1;
if source.in_string_or_comment(line_number, column) {
continue;
}
let method = cap.get(1).unwrap().as_str();
let replacement = if method.contains("File") {
"File.exist?"
} else {
"Dir.exist?"
};
offenses.push(Offense::new(
self.name(),
format!("`{}` is deprecated. Use `{}` instead.", method, replacement),
self.severity(),
Location::new(line_number, column, full_match.len()),
));
}
}
offenses
}
}
macro_rules! simple_cop {
($name:ident, $cop_name:expr, $desc:expr) => {
pub struct $name;
impl Cop for $name {
fn name(&self) -> &str { $cop_name }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str { $desc }
fn check(&self, _source: &SourceFile) -> Vec<Offense> {
Vec::new() }
}
};
}
simple_cop!(DuplicateBranch, "Lint/DuplicateBranch", "Duplicate code in if/else branches");
simple_cop!(DuplicateCaseCondition, "Lint/DuplicateCaseCondition", "Duplicate case condition");
simple_cop!(DuplicateElsifCondition, "Lint/DuplicateElsifCondition", "Duplicate elsif condition");
simple_cop!(DuplicateHashKey, "Lint/DuplicateHashKey", "Duplicate hash key");
simple_cop!(DuplicateMagicComment, "Lint/DuplicateMagicComment", "Duplicate magic comment");
simple_cop!(DuplicateRequire, "Lint/DuplicateRequire", "Duplicate require");
simple_cop!(DuplicateRescueException, "Lint/DuplicateRescueException", "Duplicate rescue exception");
pub struct EmptyBlock;
impl Cop for EmptyBlock {
fn name(&self) -> &str { "Lint/EmptyBlock" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str { "Checks for empty blocks" }
fn check(&self, source: &SourceFile) -> Vec<Offense> {
let mut offenses = Vec::new();
for (line_num, line) in source.lines.iter().enumerate() {
let line_number = line_num + 1;
for mat in EMPTY_BLOCK.find_iter(line) {
let column = mat.start() + 1;
if source.in_string_or_comment(line_number, column) {
continue;
}
offenses.push(Offense::new(
self.name(),
"Empty block detected.",
self.severity(),
Location::new(line_number, column, mat.len()),
));
}
}
offenses
}
}
simple_cop!(EmptyClass, "Lint/EmptyClass", "Empty class definition");
simple_cop!(EmptyConditionalBody, "Lint/EmptyConditionalBody", "Empty conditional body");
simple_cop!(EmptyEnsure, "Lint/EmptyEnsure", "Empty ensure block");
pub struct EmptyFile;
impl Cop for EmptyFile {
fn name(&self) -> &str { "Lint/EmptyFile" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str { "Checks for empty files" }
fn check(&self, source: &SourceFile) -> Vec<Offense> {
if source.is_empty() {
vec![Offense::new(
self.name(),
"Empty file detected.",
self.severity(),
Location::new(1, 1, 0),
)]
} else {
Vec::new()
}
}
}
pub struct EmptyInterpolation;
impl Cop for EmptyInterpolation {
fn name(&self) -> &str { "Lint/EmptyInterpolation" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str { "Checks for empty interpolation" }
fn check(&self, source: &SourceFile) -> Vec<Offense> {
let mut offenses = Vec::new();
for (line_num, line) in source.lines.iter().enumerate() {
let line_number = line_num + 1;
for mat in EMPTY_INTERPOLATION.find_iter(line) {
let column = mat.start() + 1;
offenses.push(Offense::new(
self.name(),
"Empty interpolation `#{}` detected.",
self.severity(),
Location::new(line_number, column, mat.len()),
));
}
}
offenses
}
}
simple_cop!(EmptyWhen, "Lint/EmptyWhen", "Empty when clause");
simple_cop!(EnsureReturn, "Lint/EnsureReturn", "Return in ensure block");
simple_cop!(FlipFlop, "Lint/FlipFlop", "Flip-flop operator usage");
pub struct FloatComparison;
impl Cop for FloatComparison {
fn name(&self) -> &str { "Lint/FloatComparison" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str { "Checks for float comparison with ==" }
fn check(&self, source: &SourceFile) -> Vec<Offense> {
let mut offenses = Vec::new();
for (line_num, line) in source.lines.iter().enumerate() {
let line_number = line_num + 1;
if line.contains(".0") || line.contains("Float") || line.contains("float")
|| line.contains(".to_f") || line.contains("\\.") && line.contains("==") {
for cap in FLOAT_COMPARISON.captures_iter(line) {
let full_match = cap.get(0).unwrap();
let matched_text = full_match.as_str();
let has_decimal = matched_text.contains('.');
let has_to_f = line.contains(".to_f");
let has_float_keyword = line.contains("Float") || line.contains("float");
if !has_decimal && !has_to_f && !has_float_keyword {
continue;
}
let column = full_match.start() + 1;
if source.in_string_or_comment(line_number, column) {
continue;
}
offenses.push(Offense::new(
self.name(),
"Avoid comparing floats with `==`. Use `.round` or tolerance check.",
self.severity(),
Location::new(line_number, column, full_match.len()),
));
}
}
}
offenses
}
}
simple_cop!(FloatOutOfRange, "Lint/FloatOutOfRange", "Float literal out of range");
simple_cop!(FormatParameterMismatch, "Lint/FormatParameterMismatch", "Format string parameter mismatch");
simple_cop!(IdentityComparison, "Lint/IdentityComparison", "Identity comparison issue");
simple_cop!(ImplicitStringConcatenation, "Lint/ImplicitStringConcatenation", "Implicit string concatenation");
simple_cop!(InheritException, "Lint/InheritException", "Inherit from StandardError not Exception");
simple_cop!(LiteralInInterpolation, "Lint/LiteralInInterpolation", "Literal in interpolation");
pub struct Loop;
impl Cop for Loop {
fn name(&self) -> &str { "Lint/Loop" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str { "Use `loop` instead of `while true`" }
fn check(&self, source: &SourceFile) -> Vec<Offense> {
let mut offenses = Vec::new();
for (line_num, line) in source.lines.iter().enumerate() {
let line_number = line_num + 1;
for mat in LOOP_WHILE_TRUE.find_iter(line) {
let column = mat.start() + 1;
if source.in_string_or_comment(line_number, column) {
continue;
}
offenses.push(Offense::new(
self.name(),
"Use `loop` instead of `while true` or `until false`.",
self.severity(),
Location::new(line_number, column, mat.len()),
));
}
}
offenses
}
}
simple_cop!(MissingSuper, "Lint/MissingSuper", "Missing super call");
simple_cop!(NestedMethodDefinition, "Lint/NestedMethodDefinition", "Method defined inside method");
simple_cop!(NestedPercentLiteral, "Lint/NestedPercentLiteral", "Nested percent literal");
simple_cop!(NonLocalExitFromIterator, "Lint/NonLocalExitFromIterator", "Return from block");
simple_cop!(NumberConversion, "Lint/NumberConversion", "Use Integer() not .to_i");
simple_cop!(OrAssignmentToConstant, "Lint/OrAssignmentToConstant", "Or-assignment to constant");
simple_cop!(OrderedMagicComments, "Lint/OrderedMagicComments", "Magic comments in wrong order");
simple_cop!(ParenthesesAsGroupedExpression, "Lint/ParenthesesAsGroupedExpression", "Ambiguous parentheses");
simple_cop!(PercentStringArray, "Lint/PercentStringArray", "Percent string array issue");
simple_cop!(PercentSymbolArray, "Lint/PercentSymbolArray", "Percent symbol array issue");
simple_cop!(RaiseException, "Lint/RaiseException", "Raise StandardError not Exception");
pub struct RandOne;
impl Cop for RandOne {
fn name(&self) -> &str { "Lint/RandOne" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str { "rand(1) always returns 0" }
fn check(&self, source: &SourceFile) -> Vec<Offense> {
let mut offenses = Vec::new();
for (line_num, line) in source.lines.iter().enumerate() {
let line_number = line_num + 1;
for mat in RAND_ONE.find_iter(line) {
let column = mat.start() + 1;
if source.in_string_or_comment(line_number, column) {
continue;
}
offenses.push(Offense::new(
self.name(),
"`rand(1)` always returns 0.",
self.severity(),
Location::new(line_number, column, mat.len()),
));
}
}
offenses
}
}
simple_cop!(RedundantCopDisableDirective, "Lint/RedundantCopDisableDirective", "Redundant cop disable");
simple_cop!(RedundantRequireStatement, "Lint/RedundantRequireStatement", "Redundant require");
pub struct RedundantStringCoercion;
impl Cop for RedundantStringCoercion {
fn name(&self) -> &str { "Lint/RedundantStringCoercion" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str { "Redundant .to_s in interpolation" }
fn check(&self, source: &SourceFile) -> Vec<Offense> {
let mut offenses = Vec::new();
for (line_num, line) in source.lines.iter().enumerate() {
let line_number = line_num + 1;
for mat in REDUNDANT_STRING_COERCION.find_iter(line) {
let column = mat.start() + 1;
offenses.push(Offense::new(
self.name(),
"Redundant `.to_s` in string interpolation.",
self.severity(),
Location::new(line_number, column, mat.len()),
));
}
}
offenses
}
}
simple_cop!(RedundantWithIndex, "Lint/RedundantWithIndex", "Redundant with_index(0)");
simple_cop!(RedundantWithObject, "Lint/RedundantWithObject", "Redundant with_object");
simple_cop!(RegexpAsCondition, "Lint/RegexpAsCondition", "Regexp used as condition");
pub struct RescueException;
impl Cop for RescueException {
fn name(&self) -> &str { "Lint/RescueException" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str { "Rescuing Exception is too broad" }
fn check(&self, source: &SourceFile) -> Vec<Offense> {
let mut offenses = Vec::new();
for (line_num, line) in source.lines.iter().enumerate() {
let line_number = line_num + 1;
for cap in RESCUE_EXCEPTION.captures_iter(line) {
let full_match = cap.get(0).unwrap();
let column = full_match.start() + 1;
if source.in_string_or_comment(line_number, column) {
continue;
}
offenses.push(Offense::new(
self.name(),
"Rescuing `Exception` is too broad. Rescue specific exceptions.",
self.severity(),
Location::new(line_number, column, full_match.len()),
));
}
}
offenses
}
}
simple_cop!(RescueType, "Lint/RescueType", "Wrong type in rescue");
simple_cop!(ReturnInVoidContext, "Lint/ReturnInVoidContext", "Return in void context");
simple_cop!(SafeNavigationChain, "Lint/SafeNavigationChain", "Safe navigation chain issue");
simple_cop!(ScriptPermission, "Lint/ScriptPermission", "Script permission issue");
pub struct SelfAssignment;
impl Cop for SelfAssignment {
fn name(&self) -> &str { "Lint/SelfAssignment" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str { "Self-assignment detected" }
fn check(&self, source: &SourceFile) -> Vec<Offense> {
let mut offenses = Vec::new();
for (line_num, line) in source.lines.iter().enumerate() {
let line_number = line_num + 1;
for cap in SELF_ASSIGNMENT.captures_iter(line) {
let left = cap.get(1).unwrap().as_str();
let right = cap.get(2).unwrap().as_str();
if left != right {
continue;
}
let full_match = cap.get(0).unwrap();
let column = full_match.start() + 1;
if source.in_string_or_comment(line_number, column) {
continue;
}
offenses.push(Offense::new(
self.name(),
format!("Self-assignment detected: `{}`", full_match.as_str()),
self.severity(),
Location::new(line_number, column, full_match.len()),
));
}
}
offenses
}
}
simple_cop!(ShadowedException, "Lint/ShadowedException", "Shadowed exception in rescue");
simple_cop!(ShadowingOuterLocalVariable, "Lint/ShadowingOuterLocalVariable", "Shadowing outer variable");
pub struct SuppressedException;
impl Cop for SuppressedException {
fn name(&self) -> &str { "Lint/SuppressedException" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str { "Empty rescue suppresses exceptions" }
fn check(&self, source: &SourceFile) -> Vec<Offense> {
let mut offenses = Vec::new();
for (line_num, line) in source.lines.iter().enumerate() {
let line_number = line_num + 1;
if line.trim() == "rescue" {
offenses.push(Offense::new(
self.name(),
"Empty rescue suppresses exceptions. Handle or log them.",
self.severity(),
Location::new(line_number, 1, line.len()),
));
}
}
offenses
}
}
simple_cop!(SymbolConversion, "Lint/SymbolConversion", "Unnecessary symbol conversion");
simple_cop!(ToJSON, "Lint/ToJSON", "to_json method signature issue");
simple_cop!(TopLevelReturnWithArgument, "Lint/TopLevelReturnWithArgument", "Return with arg at top level");
simple_cop!(TrailingCommaInAttributeDeclaration, "Lint/TrailingCommaInAttributeDeclaration", "Trailing comma in attr");
simple_cop!(TripleQuotes, "Lint/TripleQuotes", "Triple quotes detected");
simple_cop!(UnderscorePrefixedVariableName, "Lint/UnderscorePrefixedVariableName", "Underscore-prefixed var used");
simple_cop!(UnifiedInteger, "Lint/UnifiedInteger", "Use Integer not Fixnum/Bignum");
simple_cop!(UnreachableCode, "Lint/UnreachableCode", "Unreachable code after return");
simple_cop!(UnreachableLoop, "Lint/UnreachableLoop", "Loop never repeats");
simple_cop!(UnusedBlockArgument, "Lint/UnusedBlockArgument", "Unused block argument");
simple_cop!(UnusedMethodArgument, "Lint/UnusedMethodArgument", "Unused method argument");
simple_cop!(UselessAssignment, "Lint/UselessAssignment", "Assigned but never used");
simple_cop!(UselessMethodDefinition, "Lint/UselessMethodDefinition", "Method just calls super");
simple_cop!(Void, "Lint/Void", "Void value expression");
simple_cop!(LiteralAssignmentInCondition, "Lint/LiteralAssignmentInCondition", "Literal assignment in condition");
simple_cop!(SharedMutableDefault, "Lint/SharedMutableDefault", "Shared mutable default");
simple_cop!(MixedCaseRange, "Lint/MixedCaseRange", "Mixed case in range");
simple_cop!(ItWithoutArgumentsInBlock, "Lint/ItWithoutArgumentsInBlock", "it without args");
simple_cop!(DuplicateSetElement, "Lint/DuplicateSetElement", "Duplicate element in Set");
simple_cop!(DuplicateMatchPattern, "Lint/DuplicateMatchPattern", "Duplicate match pattern");
simple_cop!(DuplicateRegexpCharacterClassElement, "Lint/DuplicateRegexpCharacterClassElement", "Duplicate in char class");
simple_cop!(EmptyExpression, "Lint/EmptyExpression", "Empty expression");
simple_cop!(EmptyInPattern, "Lint/EmptyInPattern", "Empty in-pattern");
simple_cop!(HashCompareByIdentity, "Lint/HashCompareByIdentity", "Hash compare_by_identity issue");
simple_cop!(IneffectiveAccessModifier, "Lint/IneffectiveAccessModifier", "Access modifier has no effect");
simple_cop!(InterpolationCheck, "Lint/InterpolationCheck", "Interpolation in single-quoted string");
simple_cop!(MissingCopEnableDirective, "Lint/MissingCopEnableDirective", "Missing cop enable");
simple_cop!(MultipleComparison, "Lint/MultipleComparison", "Use .include? instead");
simple_cop!(NextWithoutAccumulator, "Lint/NextWithoutAccumulator", "next without accumulator");
simple_cop!(NoReturnInBeginEndBlocks, "Lint/NoReturnInBeginEndBlocks", "Return in BEGIN/END");
simple_cop!(NonAtomicFileOperation, "Lint/NonAtomicFileOperation", "Non-atomic file operation");
simple_cop!(NumberedParameterAssignment, "Lint/NumberedParameterAssignment", "Assignment to numbered param");
simple_cop!(OutOfRangeRegexpRef, "Lint/OutOfRangeRegexpRef", "Regexp ref beyond captures");
simple_cop!(RedundantSafeNavigation, "Lint/RedundantSafeNavigation", "Redundant safe navigation");
simple_cop!(RedundantSplatExpansion, "Lint/RedundantSplatExpansion", "Redundant splat");
simple_cop!(RequireParentheses, "Lint/RequireParentheses", "Require parentheses");
simple_cop!(SafeNavigationConsistency, "Lint/SafeNavigationConsistency", "Safe navigation consistency");
simple_cop!(SendWithMixinArgument, "Lint/SendWithMixinArgument", "send with include/extend");
simple_cop!(ShadowedArgument, "Lint/ShadowedArgument", "Reassigned method argument");
simple_cop!(StructNewOverride, "Lint/StructNewOverride", "Override in Struct.new");
simple_cop!(SuppressedExceptionInNumberConversion, "Lint/SuppressedExceptionInNumberConversion", "Suppressed in conversion");
simple_cop!(Syntax, "Lint/Syntax", "Syntax errors");
simple_cop!(ToEnumArguments, "Lint/ToEnumArguments", "to_enum arguments");
simple_cop!(UnexpectedBlockArity, "Lint/UnexpectedBlockArity", "Wrong block arity");
simple_cop!(UnmodifiedReduceAccumulator, "Lint/UnmodifiedReduceAccumulator", "Unmodified accumulator");
simple_cop!(UriEscapeUnescape, "Lint/UriEscapeUnescape", "Deprecated URI methods");
simple_cop!(UriRegexp, "Lint/UriRegexp", "Use URI::DEFAULT_PARSER");
simple_cop!(UselessAccessModifier, "Lint/UselessAccessModifier", "Access modifier with no methods after");
simple_cop!(UselessDefined, "Lint/UselessDefined", "Useless defined?");
simple_cop!(UselessElseWithoutRescue, "Lint/UselessElseWithoutRescue", "else without rescue");
simple_cop!(UselessRescue, "Lint/UselessRescue", "Rescue that just re-raises");
simple_cop!(UselessSetterCall, "Lint/UselessSetterCall", "Setter on local");
simple_cop!(UselessTimes, "Lint/UselessTimes", "0.times or 1.times");
simple_cop!(UselessOr, "Lint/UselessOr", "x || x");
simple_cop!(UselessConstantScoping, "Lint/UselessConstantScoping", "Constant scope issue");
simple_cop!(UselessDefaultValueArgument, "Lint/UselessDefaultValueArgument", "Useless default value");
simple_cop!(UselessNumericOperation, "Lint/UselessNumericOperation", "x * 1 or x + 0");
simple_cop!(UselessRuby2Keywords, "Lint/UselessRuby2Keywords", "Useless ruby2_keywords");
simple_cop!(RedundantDirGlobSort, "Lint/RedundantDirGlobSort", "Redundant sort on Dir.glob");
simple_cop!(RedundantRegexpQuantifiers, "Lint/RedundantRegexpQuantifiers", "Redundant quantifiers");
simple_cop!(RedundantTypeConversion, "Lint/RedundantTypeConversion", "Redundant type conversion");
simple_cop!(RequireRangeParentheses, "Lint/RequireRangeParentheses", "Range needs parens");
simple_cop!(RequireRelativeSelfPath, "Lint/RequireRelativeSelfPath", "require_relative self");
simple_cop!(RefinementImportMethods, "Lint/RefinementImportMethods", "Refinement import");
simple_cop!(LambdaWithoutLiteralBlock, "Lint/LambdaWithoutLiteralBlock", "Lambda without block");
simple_cop!(HeredocMethodCallPosition, "Lint/HeredocMethodCallPosition", "Heredoc method call position");
simple_cop!(ErbNewArguments, "Lint/ErbNewArguments", "Erb.new argument changes");
simple_cop!(AmbiguousBlockAssociation, "Lint/AmbiguousBlockAssociation", "Ambiguous block association");
simple_cop!(AmbiguousOperatorPrecedence, "Lint/AmbiguousOperatorPrecedence", "Ambiguous precedence");
simple_cop!(ConstantOverwrittenInRescue, "Lint/ConstantOverwrittenInRescue", "Constant overwritten in rescue");
simple_cop!(ConstantResolution, "Lint/ConstantResolution", "Constant resolution style");
simple_cop!(CopDirectiveSyntax, "Lint/CopDirectiveSyntax", "Cop directive syntax");
simple_cop!(EachWithObjectArgument, "Lint/EachWithObjectArgument", "Wrong argument to each_with_object");
simple_cop!(ElseLayout, "Lint/ElseLayout", "else layout issue");
simple_cop!(CircularArgumentReference, "Lint/CircularArgumentReference", "Circular default arg");
simple_cop!(IncompatibleIoSelectWithFiberScheduler, "Lint/IncompatibleIoSelectWithFiberScheduler", "IO.select compat");
simple_cop!(HashNewWithKeywordArgumentsAsDefault, "Lint/HashNewWithKeywordArgumentsAsDefault", "Hash.new with kwargs");
simple_cop!(NonDeterministicRequireOrder, "Lint/NonDeterministicRequireOrder", "Dir.glob without sort");
simple_cop!(SafeNavigationWithEmpty, "Lint/SafeNavigationWithEmpty", "&.empty? issue");
simple_cop!(ArrayLiteralInRegexp, "Lint/ArrayLiteralInRegexp", "Array literal in regexp");
simple_cop!(DeprecatedConstants, "Lint/DeprecatedConstants", "Use of deprecated constants");
simple_cop!(DeprecatedOpenSSLConstant, "Lint/DeprecatedOpenSSLConstant", "Use of deprecated OpenSSL constants");
simple_cop!(MixedRegexpCaptureTypes, "Lint/MixedRegexpCaptureTypes", "Mixed named and numbered captures in regexp");
simple_cop!(NumericOperationWithConstantResult, "Lint/NumericOperationWithConstantResult", "Numeric operation with constant result");
simple_cop!(RedundantCopEnableDirective, "Lint/RedundantCopEnableDirective", "Redundant rubocop:enable directive");
simple_cop!(UnescapedBracketInRegexp, "Lint/UnescapedBracketInRegexp", "Unescaped bracket in regexp");
simple_cop!(LiteralAsCondition, "Lint/LiteralAsCondition", "Literal used as condition");
pub struct Derived;
impl Cop for Derived {
fn name(&self) -> &str { "Lint/Derived" }
fn category(&self) -> Category { Category::Lint }
fn severity(&self) -> Severity { Severity::Warning }
fn description(&self) -> &str { "Deprecated Derived cop" }
fn check(&self, _source: &SourceFile) -> Vec<Offense> {
Vec::new()
}
}
pub fn all_lint_extra_cops() -> Vec<Box<dyn Cop>> {
vec![
Box::new(AmbiguousAssignment),
Box::new(AmbiguousOperator),
Box::new(AmbiguousRegexpLiteral),
Box::new(AmbiguousRange),
Box::new(AssignmentInCondition),
Box::new(BigDecimalNew),
Box::new(BinaryOperatorWithIdenticalOperands),
Box::new(BooleanSymbol),
Box::new(ConstantDefinitionInBlock),
Box::new(ConstantReassignment),
Box::new(DeprecatedClassMethods),
Box::new(EmptyBlock),
Box::new(EmptyFile),
Box::new(EmptyInterpolation),
Box::new(FloatComparison),
Box::new(Loop),
Box::new(RandOne),
Box::new(RedundantStringCoercion),
Box::new(RescueException),
Box::new(SelfAssignment),
Box::new(SuppressedException),
Box::new(Derived),
Box::new(DuplicateBranch),
Box::new(DuplicateCaseCondition),
Box::new(DuplicateElsifCondition),
Box::new(DuplicateHashKey),
Box::new(DuplicateMagicComment),
Box::new(DuplicateRequire),
Box::new(DuplicateRescueException),
Box::new(EmptyClass),
Box::new(EmptyConditionalBody),
Box::new(EmptyEnsure),
Box::new(EmptyWhen),
Box::new(EnsureReturn),
Box::new(FlipFlop),
Box::new(FloatOutOfRange),
Box::new(FormatParameterMismatch),
Box::new(IdentityComparison),
Box::new(ImplicitStringConcatenation),
Box::new(InheritException),
Box::new(LiteralInInterpolation),
Box::new(MissingSuper),
Box::new(NestedMethodDefinition),
Box::new(NestedPercentLiteral),
Box::new(NonLocalExitFromIterator),
Box::new(NumberConversion),
Box::new(OrAssignmentToConstant),
Box::new(OrderedMagicComments),
Box::new(ParenthesesAsGroupedExpression),
Box::new(PercentStringArray),
Box::new(PercentSymbolArray),
Box::new(RaiseException),
Box::new(RedundantCopDisableDirective),
Box::new(RedundantRequireStatement),
Box::new(RedundantWithIndex),
Box::new(RedundantWithObject),
Box::new(RegexpAsCondition),
Box::new(RescueType),
Box::new(ReturnInVoidContext),
Box::new(SafeNavigationChain),
Box::new(ScriptPermission),
Box::new(ShadowedException),
Box::new(ShadowingOuterLocalVariable),
Box::new(SymbolConversion),
Box::new(ToJSON),
Box::new(TopLevelReturnWithArgument),
Box::new(TrailingCommaInAttributeDeclaration),
Box::new(TripleQuotes),
Box::new(UnderscorePrefixedVariableName),
Box::new(UnifiedInteger),
Box::new(UnreachableCode),
Box::new(UnreachableLoop),
Box::new(UnusedBlockArgument),
Box::new(UnusedMethodArgument),
Box::new(UselessAssignment),
Box::new(UselessMethodDefinition),
Box::new(Void),
Box::new(LiteralAssignmentInCondition),
Box::new(SharedMutableDefault),
Box::new(MixedCaseRange),
Box::new(ItWithoutArgumentsInBlock),
Box::new(DuplicateSetElement),
Box::new(DuplicateMatchPattern),
Box::new(DuplicateRegexpCharacterClassElement),
Box::new(EmptyExpression),
Box::new(EmptyInPattern),
Box::new(HashCompareByIdentity),
Box::new(IneffectiveAccessModifier),
Box::new(InterpolationCheck),
Box::new(MissingCopEnableDirective),
Box::new(MultipleComparison),
Box::new(NextWithoutAccumulator),
Box::new(NoReturnInBeginEndBlocks),
Box::new(NonAtomicFileOperation),
Box::new(NumberedParameterAssignment),
Box::new(OutOfRangeRegexpRef),
Box::new(RedundantSafeNavigation),
Box::new(RedundantSplatExpansion),
Box::new(RequireParentheses),
Box::new(SafeNavigationConsistency),
Box::new(SendWithMixinArgument),
Box::new(ShadowedArgument),
Box::new(StructNewOverride),
Box::new(SuppressedExceptionInNumberConversion),
Box::new(Syntax),
Box::new(ToEnumArguments),
Box::new(UnexpectedBlockArity),
Box::new(UnmodifiedReduceAccumulator),
Box::new(UriEscapeUnescape),
Box::new(UriRegexp),
Box::new(UselessAccessModifier),
Box::new(UselessDefined),
Box::new(UselessElseWithoutRescue),
Box::new(UselessRescue),
Box::new(UselessSetterCall),
Box::new(UselessTimes),
Box::new(UselessOr),
Box::new(UselessConstantScoping),
Box::new(UselessDefaultValueArgument),
Box::new(UselessNumericOperation),
Box::new(UselessRuby2Keywords),
Box::new(RedundantDirGlobSort),
Box::new(RedundantRegexpQuantifiers),
Box::new(RedundantTypeConversion),
Box::new(RequireRangeParentheses),
Box::new(RequireRelativeSelfPath),
Box::new(RefinementImportMethods),
Box::new(LambdaWithoutLiteralBlock),
Box::new(HeredocMethodCallPosition),
Box::new(ErbNewArguments),
Box::new(AmbiguousBlockAssociation),
Box::new(AmbiguousOperatorPrecedence),
Box::new(ConstantOverwrittenInRescue),
Box::new(ConstantResolution),
Box::new(CopDirectiveSyntax),
Box::new(EachWithObjectArgument),
Box::new(ElseLayout),
Box::new(CircularArgumentReference),
Box::new(IncompatibleIoSelectWithFiberScheduler),
Box::new(HashNewWithKeywordArgumentsAsDefault),
Box::new(NonDeterministicRequireOrder),
Box::new(SafeNavigationWithEmpty),
Box::new(ArrayLiteralInRegexp),
Box::new(DeprecatedConstants),
Box::new(DeprecatedOpenSSLConstant),
Box::new(MixedRegexpCaptureTypes),
Box::new(NumericOperationWithConstantResult),
Box::new(RedundantCopEnableDirective),
Box::new(UnescapedBracketInRegexp),
Box::new(LiteralAsCondition),
]
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn test_source(content: &str) -> SourceFile {
SourceFile::from_string(PathBuf::from("test.rb"), content.to_string())
}
#[test]
fn test_ambiguous_assignment() {
let cop = AmbiguousAssignment;
let source = test_source("x =+ 1\n");
let offenses = cop.check(&source);
assert_eq!(offenses.len(), 1);
assert!(offenses[0].message.contains("=+"));
}
#[test]
fn test_ambiguous_assignment_correct() {
let cop = AmbiguousAssignment;
let source = test_source("x += 1\n");
let offenses = cop.check(&source);
assert_eq!(offenses.len(), 0);
}
#[test]
fn test_bigdecimal_new() {
let cop = BigDecimalNew;
let source = test_source("x = BigDecimal.new('1.5')\n");
let offenses = cop.check(&source);
assert_eq!(offenses.len(), 1);
assert!(offenses[0].message.contains("BigDecimal()"));
}
#[test]
fn test_binary_identical_operands() {
let cop = BinaryOperatorWithIdenticalOperands;
let source = test_source("if x == x\nend\n");
let offenses = cop.check(&source);
assert_eq!(offenses.len(), 1);
}
#[test]
fn test_boolean_symbol() {
let cop = BooleanSymbol;
let source = test_source("hash = { value: :true }\n");
let offenses = cop.check(&source);
assert_eq!(offenses.len(), 1);
}
#[test]
fn test_deprecated_class_methods() {
let cop = DeprecatedClassMethods;
let source = test_source("if File.exists?('path')\nend\n");
let offenses = cop.check(&source);
assert_eq!(offenses.len(), 1);
assert!(offenses[0].message.contains("File.exist?"));
}
#[test]
fn test_empty_block() {
let cop = EmptyBlock;
let source = test_source("items.each { }\n");
let offenses = cop.check(&source);
assert_eq!(offenses.len(), 1);
}
#[test]
fn test_empty_file() {
let cop = EmptyFile;
let source = test_source("");
let offenses = cop.check(&source);
assert_eq!(offenses.len(), 1);
}
#[test]
fn test_empty_interpolation() {
let cop = EmptyInterpolation;
let source = test_source("\"Hello #{} world\"\n");
let offenses = cop.check(&source);
assert_eq!(offenses.len(), 1);
}
#[test]
fn test_float_comparison() {
let cop = FloatComparison;
let source = test_source("if value.to_f == 3.14\nend\n");
let offenses = cop.check(&source);
assert_eq!(offenses.len(), 1);
}
#[test]
fn test_loop_while_true() {
let cop = Loop;
let source = test_source("while true\n break if done\nend\n");
let offenses = cop.check(&source);
assert_eq!(offenses.len(), 1);
assert!(offenses[0].message.contains("loop"));
}
#[test]
fn test_rand_one() {
let cop = RandOne;
let source = test_source("x = rand(1)\n");
let offenses = cop.check(&source);
assert_eq!(offenses.len(), 1);
}
#[test]
fn test_redundant_string_coercion() {
let cop = RedundantStringCoercion;
let source = test_source("\"Value: #{x.to_s}\"\n");
let offenses = cop.check(&source);
assert_eq!(offenses.len(), 1);
}
#[test]
fn test_rescue_exception() {
let cop = RescueException;
let source = test_source("begin\n risky\nrescue Exception\nend\n");
let offenses = cop.check(&source);
assert_eq!(offenses.len(), 1);
}
#[test]
fn test_self_assignment() {
let cop = SelfAssignment;
let source = test_source("x = x\n");
let offenses = cop.check(&source);
assert_eq!(offenses.len(), 1);
}
#[test]
fn test_suppressed_exception() {
let cop = SuppressedException;
let source = test_source("begin\n risky\nrescue\nend\n");
let offenses = cop.check(&source);
assert_eq!(offenses.len(), 1);
}
}