use crate::cop::{Cop, CopConfig};
use crate::diagnostic::Diagnostic;
use crate::parse::source::SourceFile;
pub struct CommentAnnotation;
impl Cop for CommentAnnotation {
fn name(&self) -> &'static str {
"Style/CommentAnnotation"
}
fn uses_line_phase(&self) -> bool {
true
}
fn check_lines(
&self,
source: &SourceFile,
config: &CopConfig,
diagnostics: &mut Vec<Diagnostic>,
_corrections: Option<&mut Vec<crate::correction::Correction>>,
) {
let require_colon = config.get_bool("RequireColon", true);
let lines: Vec<String> = source
.lines()
.map(|l| String::from_utf8_lossy(l).into_owned())
.collect();
for (idx, line) in lines.iter().enumerate() {
if !first_comment_or_inline(&lines, idx, line) {
continue;
}
check_line(self, source, idx, line, require_colon, diagnostics);
}
}
}
fn first_comment_or_inline(lines: &[String], idx: usize, line: &str) -> bool {
let trimmed = line.trim_start();
if !trimmed.starts_with('#') {
return false;
}
let before = &line[..line.len() - trimmed.len()];
if before.chars().any(|c| !c.is_whitespace()) {
return true;
}
if idx == 0 {
return true;
}
!lines[idx - 1].trim_start().starts_with('#')
}
fn check_line(
cop: &CommentAnnotation,
source: &SourceFile,
idx: usize,
line: &str,
require_colon: bool,
diagnostics: &mut Vec<Diagnostic>,
) {
let trimmed = line.trim_start();
let body = if let Some(rest) = trimmed.strip_prefix("# ") {
rest
} else if let Some(rest) = trimmed.strip_prefix('#') {
if rest.starts_with(char::is_whitespace) {
return;
}
rest
} else {
return;
};
for kw in ["TODO", "FIXME", "OPTIMIZE", "HACK", "REVIEW", "NOTE"] {
if let Some(msg) = annotation_msg(body, kw, require_colon) {
let (line_n, col) =
source.offset_to_line_col(source.line_start(idx + 1).unwrap_or(0));
diagnostics.push(cop.diagnostic(source, line_n, col, msg));
return;
}
}
}
fn annotation_msg(body: &str, kw: &str, require_colon: bool) -> Option<String> {
if !body.starts_with(kw) {
return None;
}
let rest = &body[kw.len()..];
let (has_colon, after) = if let Some(r) = rest.strip_prefix(':') {
(true, r)
} else if rest.starts_with(char::is_whitespace) {
(false, rest.trim_start())
} else {
return None;
};
if after.is_empty() {
return None;
}
if require_colon == has_colon {
if has_colon && !rest.starts_with(": ") && rest.starts_with(':') {
return Some(format!(
"Annotation keywords like `{kw}` should be followed by a colon."
));
}
return None;
}
Some(format!(
"Annotation keywords like `{kw}` should be followed by a colon."
))
}
#[cfg(test)]
mod tests {
use super::*;
crate::cop_fixture_tests!(CommentAnnotation, "cops/style/comment_annotation");
}