use std::collections::{HashMap, HashSet};
use crate::analyzer::helmlint::types::RuleCode;
#[derive(Debug, Clone, Default)]
pub struct PragmaState {
pub file_ignores: HashSet<String>,
pub line_ignores: HashMap<u32, HashSet<String>>,
pub file_disabled: bool,
}
impl PragmaState {
pub fn new() -> Self {
Self::default()
}
pub fn is_ignored(&self, code: &RuleCode, line: u32) -> bool {
if self.file_disabled {
return true;
}
if self.file_ignores.contains(code.as_str()) {
return true;
}
if let Some(ignores) = self.line_ignores.get(&line)
&& ignores.contains(code.as_str())
{
return true;
}
if line > 1
&& let Some(ignores) = self.line_ignores.get(&(line - 1))
&& ignores.contains(code.as_str())
{
return true;
}
false
}
pub fn add_file_ignore(&mut self, code: impl Into<String>) {
self.file_ignores.insert(code.into());
}
pub fn add_line_ignore(&mut self, line: u32, code: impl Into<String>) {
self.line_ignores
.entry(line)
.or_default()
.insert(code.into());
}
pub fn disable_file(&mut self) {
self.file_disabled = true;
}
}
pub fn extract_yaml_pragmas(content: &str) -> PragmaState {
let mut state = PragmaState::new();
for (line_num, line) in content.lines().enumerate() {
let line_number = (line_num + 1) as u32;
let trimmed = line.trim();
if let Some(comment) = trimmed.strip_prefix('#') {
process_comment(comment.trim(), line_number, &mut state);
}
}
state
}
pub fn extract_template_pragmas(content: &str) -> PragmaState {
let mut state = PragmaState::new();
for (line_num, line) in content.lines().enumerate() {
let line_number = (line_num + 1) as u32;
let trimmed = line.trim();
if let Some(comment) = trimmed.strip_prefix('#') {
if !line.contains("{{") || line.find('#') < line.find("{{") {
process_comment(comment.trim(), line_number, &mut state);
}
}
}
let mut line_num: u32 = 1;
let mut i = 0;
let chars: Vec<char> = content.chars().collect();
while i < chars.len() {
if chars[i] == '\n' {
line_num += 1;
i += 1;
continue;
}
if i + 4 < chars.len()
&& chars[i] == '{'
&& chars[i + 1] == '{'
&& (chars[i + 2] == '/'
|| (chars[i + 2] == '-' && i + 5 < chars.len() && chars[i + 3] == '/'))
{
let _comment_start = i;
let comment_line = line_num;
i += 2;
if chars[i] == '-' {
i += 1;
}
i += 2;
let mut comment_content = String::new();
while i + 3 < chars.len() {
if chars[i] == '\n' {
line_num += 1;
}
if chars[i] == '*' && chars[i + 1] == '/' {
i += 2;
if i < chars.len() && chars[i] == '-' {
i += 1;
}
if i + 1 < chars.len() && chars[i] == '}' && chars[i + 1] == '}' {
i += 2;
}
break;
}
comment_content.push(chars[i]);
i += 1;
}
process_comment(comment_content.trim(), comment_line, &mut state);
continue;
}
i += 1;
}
state
}
fn process_comment(comment: &str, line: u32, state: &mut PragmaState) {
let lower = comment.to_lowercase();
if lower.starts_with("helmlint-ignore-file") || lower.starts_with("helmlint-disable-file") {
let rest = comment
.strip_prefix("helmlint-ignore-file")
.or_else(|| comment.strip_prefix("helmlint-disable-file"))
.unwrap_or("")
.trim();
if rest.is_empty() {
state.disable_file();
} else {
for code in parse_rule_list(rest) {
state.add_file_ignore(code);
}
}
return;
}
if lower.starts_with("helmlint-ignore") || lower.starts_with("helmlint-disable") {
let rest = comment
.strip_prefix("helmlint-ignore")
.or_else(|| comment.strip_prefix("helmlint-disable"))
.unwrap_or("")
.trim();
if rest.is_empty() {
state.add_line_ignore(line, "*");
} else {
for code in parse_rule_list(rest) {
state.add_line_ignore(line, code);
}
}
}
}
fn parse_rule_list(input: &str) -> Vec<String> {
input
.split([',', ' '])
.map(|s| s.trim())
.filter(|s| !s.is_empty() && s.starts_with("HL"))
.map(|s| s.to_string())
.collect()
}
pub fn starts_with_disable_file_comment(content: &str) -> bool {
for line in content.lines().take(10) {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Some(comment) = trimmed.strip_prefix('#') {
let comment_lower = comment.trim().to_lowercase();
if comment_lower.starts_with("helmlint-ignore-file")
|| comment_lower.starts_with("helmlint-disable-file")
{
let rest = comment
.trim()
.strip_prefix("helmlint-ignore-file")
.or_else(|| comment.trim().strip_prefix("helmlint-disable-file"))
.unwrap_or("")
.trim();
if rest.is_empty() {
return true;
}
}
}
if !trimmed.starts_with('#') {
break;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_yaml_pragma_ignore() {
let content = r#"
# helmlint-ignore HL1001
name: test-chart
version: 1.0.0
"#;
let state = extract_yaml_pragmas(content);
assert!(state.is_ignored(&RuleCode::new("HL1001"), 3));
assert!(!state.is_ignored(&RuleCode::new("HL1002"), 3));
}
#[test]
fn test_yaml_pragma_file_ignore() {
let content = r#"
# helmlint-ignore-file HL1001,HL1002
name: test-chart
"#;
let state = extract_yaml_pragmas(content);
assert!(state.is_ignored(&RuleCode::new("HL1001"), 3));
assert!(state.is_ignored(&RuleCode::new("HL1002"), 10));
assert!(!state.is_ignored(&RuleCode::new("HL1003"), 3));
}
#[test]
fn test_yaml_pragma_disable_file() {
let content = r#"
# helmlint-ignore-file
name: test-chart
"#;
let state = extract_yaml_pragmas(content);
assert!(state.file_disabled);
assert!(state.is_ignored(&RuleCode::new("HL1001"), 3));
assert!(state.is_ignored(&RuleCode::new("HL9999"), 100));
}
#[test]
fn test_template_pragma() {
let content = r#"
{{/* helmlint-ignore HL3001 */}}
{{ .Values.name }}
"#;
let state = extract_template_pragmas(content);
assert!(state.is_ignored(&RuleCode::new("HL3001"), 3));
}
#[test]
fn test_template_pragma_file_ignore() {
let content = r#"
{{/* helmlint-ignore-file HL3001 */}}
apiVersion: v1
kind: ConfigMap
"#;
let state = extract_template_pragmas(content);
assert!(state.is_ignored(&RuleCode::new("HL3001"), 3));
assert!(state.is_ignored(&RuleCode::new("HL3001"), 4));
}
#[test]
fn test_multiple_rules() {
let content = r#"
# helmlint-ignore HL1001, HL1002, HL1003
apiVersion: v2
"#;
let state = extract_yaml_pragmas(content);
assert!(state.is_ignored(&RuleCode::new("HL1001"), 3));
assert!(state.is_ignored(&RuleCode::new("HL1002"), 3));
assert!(state.is_ignored(&RuleCode::new("HL1003"), 3));
}
#[test]
fn test_starts_with_disable_file() {
let content = r#"# helmlint-ignore-file
apiVersion: v2
"#;
assert!(starts_with_disable_file_comment(content));
let content_with_rules = r#"# helmlint-ignore-file HL1001
apiVersion: v2
"#;
assert!(!starts_with_disable_file_comment(content_with_rules));
let content_normal = r#"apiVersion: v2
name: test
"#;
assert!(!starts_with_disable_file_comment(content_normal));
}
#[test]
fn test_disable_alias() {
let content = r#"
# helmlint-disable HL1001
apiVersion: v2
"#;
let state = extract_yaml_pragmas(content);
assert!(state.is_ignored(&RuleCode::new("HL1001"), 3));
}
}