use serde::{Deserialize, Serialize};
pub const API_VERSION: &str = "1.1";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginSpec {
pub name: String,
pub category: String,
pub description: String,
pub api_version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub severity: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub why: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bad_example: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub good_example: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub references: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_nginx_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_nginx_version: Option<String>,
}
impl PluginSpec {
pub fn new(
name: impl Into<String>,
category: impl Into<String>,
description: impl Into<String>,
) -> Self {
Self {
name: name.into(),
category: category.into(),
description: description.into(),
api_version: API_VERSION.to_string(),
severity: None,
why: None,
bad_example: None,
good_example: None,
references: None,
min_nginx_version: None,
max_nginx_version: None,
}
}
pub fn with_severity(mut self, severity: impl Into<String>) -> Self {
self.severity = Some(severity.into());
self
}
pub fn with_why(mut self, why: impl Into<String>) -> Self {
self.why = Some(why.into());
self
}
pub fn with_bad_example(mut self, example: impl Into<String>) -> Self {
self.bad_example = Some(example.into());
self
}
pub fn with_good_example(mut self, example: impl Into<String>) -> Self {
self.good_example = Some(example.into());
self
}
pub fn with_references(mut self, refs: Vec<String>) -> Self {
self.references = Some(refs);
self
}
pub fn with_min_version(mut self, version: impl Into<String>) -> Self {
self.min_nginx_version = Some(version.into());
self
}
pub fn with_max_version(mut self, version: impl Into<String>) -> Self {
self.max_nginx_version = Some(version.into());
self
}
pub fn error_builder(&self) -> ErrorBuilder {
ErrorBuilder {
rule: self.name.clone(),
category: self.category.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct ErrorBuilder {
rule: String,
category: String,
}
impl ErrorBuilder {
pub fn error(&self, message: &str, line: usize, column: usize) -> LintError {
LintError::error(&self.rule, &self.category, message, line, column)
}
pub fn warning(&self, message: &str, line: usize, column: usize) -> LintError {
LintError::warning(&self.rule, &self.category, message, line, column)
}
pub fn error_at(&self, message: &str, directive: &(impl DirectiveExt + ?Sized)) -> LintError {
self.error(message, directive.line(), directive.column())
}
pub fn warning_at(&self, message: &str, directive: &(impl DirectiveExt + ?Sized)) -> LintError {
self.warning(message, directive.line(), directive.column())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Error,
Warning,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Fix {
pub line: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub old_text: Option<String>,
pub new_text: String,
#[serde(default)]
pub delete_line: bool,
#[serde(default)]
pub insert_after: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start_offset: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end_offset: Option<usize>,
}
impl Fix {
#[deprecated(note = "Use Fix::replace_range() for offset-based fixes instead")]
pub fn delete(line: usize) -> Self {
Self {
line,
old_text: None,
new_text: String::new(),
delete_line: true,
insert_after: false,
start_offset: None,
end_offset: None,
}
}
#[deprecated(note = "Use Fix::replace_range() for offset-based fixes instead")]
pub fn insert_after(line: usize, new_text: &str) -> Self {
Self {
line,
old_text: None,
new_text: new_text.to_string(),
delete_line: false,
insert_after: true,
start_offset: None,
end_offset: None,
}
}
pub fn replace_range(start_offset: usize, end_offset: usize, new_text: &str) -> Self {
Self {
line: 0, old_text: None,
new_text: new_text.to_string(),
delete_line: false,
insert_after: false,
start_offset: Some(start_offset),
end_offset: Some(end_offset),
}
}
pub fn is_range_based(&self) -> bool {
self.start_offset.is_some() && self.end_offset.is_some()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LintError {
pub rule: String,
pub category: String,
pub message: String,
pub severity: Severity,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub line: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub column: Option<usize>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fixes: Vec<Fix>,
}
impl LintError {
pub fn error(rule: &str, category: &str, message: &str, line: usize, column: usize) -> Self {
Self {
rule: rule.to_string(),
category: category.to_string(),
message: message.to_string(),
severity: Severity::Error,
line: if line > 0 { Some(line) } else { None },
column: if column > 0 { Some(column) } else { None },
fixes: Vec::new(),
}
}
pub fn warning(rule: &str, category: &str, message: &str, line: usize, column: usize) -> Self {
Self {
rule: rule.to_string(),
category: category.to_string(),
message: message.to_string(),
severity: Severity::Warning,
line: if line > 0 { Some(line) } else { None },
column: if column > 0 { Some(column) } else { None },
fixes: Vec::new(),
}
}
pub fn with_fix(mut self, fix: Fix) -> Self {
self.fixes.push(fix);
self
}
pub fn with_fixes(mut self, fixes: Vec<Fix>) -> Self {
self.fixes.extend(fixes);
self
}
}
pub trait Plugin: Default {
fn spec(&self) -> PluginSpec;
fn check(&self, config: &Config, path: &str) -> Vec<LintError>;
}
pub use nginx_lint_common::parser::ast::{
Argument, ArgumentValue, Block, Comment, Config, ConfigItem, Directive, Position, Span,
};
pub use nginx_lint_common::parser::context::{AllDirectivesWithContextIter, DirectiveWithContext};
pub trait ConfigExt {
fn all_directives(&self) -> nginx_lint_common::parser::ast::AllDirectives<'_>;
fn all_directives_with_context(&self) -> AllDirectivesWithContextIter<'_>;
fn is_included_from(&self, context: &str) -> bool;
fn is_included_from_http(&self) -> bool;
fn is_included_from_http_server(&self) -> bool;
fn is_included_from_http_location(&self) -> bool;
fn is_included_from_stream(&self) -> bool;
fn immediate_parent_context(&self) -> Option<&str>;
}
impl ConfigExt for Config {
fn all_directives(&self) -> nginx_lint_common::parser::ast::AllDirectives<'_> {
Config::all_directives(self)
}
fn all_directives_with_context(&self) -> AllDirectivesWithContextIter<'_> {
Config::all_directives_with_context(self)
}
fn is_included_from(&self, context: &str) -> bool {
Config::is_included_from(self, context)
}
fn is_included_from_http(&self) -> bool {
Config::is_included_from_http(self)
}
fn is_included_from_http_server(&self) -> bool {
Config::is_included_from_http_server(self)
}
fn is_included_from_http_location(&self) -> bool {
Config::is_included_from_http_location(self)
}
fn is_included_from_stream(&self) -> bool {
Config::is_included_from_stream(self)
}
fn immediate_parent_context(&self) -> Option<&str> {
Config::immediate_parent_context(self)
}
}
pub trait DirectiveExt {
fn is(&self, name: &str) -> bool;
fn first_arg(&self) -> Option<&str>;
fn first_arg_is(&self, value: &str) -> bool;
fn arg_at(&self, index: usize) -> Option<&str>;
fn last_arg(&self) -> Option<&str>;
fn has_arg(&self, value: &str) -> bool;
fn arg_count(&self) -> usize;
fn line(&self) -> usize;
fn column(&self) -> usize;
fn full_start_offset(&self) -> usize;
fn replace_with(&self, new_text: &str) -> Fix;
fn delete_line(&self) -> Fix;
fn insert_after(&self, new_text: &str) -> Fix;
fn insert_after_many(&self, lines: &[&str]) -> Fix;
fn insert_before(&self, new_text: &str) -> Fix;
fn insert_before_many(&self, lines: &[&str]) -> Fix;
}
impl DirectiveExt for Directive {
fn is(&self, name: &str) -> bool {
self.name == name
}
fn first_arg(&self) -> Option<&str> {
self.args.first().map(|a| a.as_str())
}
fn first_arg_is(&self, value: &str) -> bool {
self.first_arg() == Some(value)
}
fn arg_at(&self, index: usize) -> Option<&str> {
self.args.get(index).map(|a| a.as_str())
}
fn last_arg(&self) -> Option<&str> {
self.args.last().map(|a| a.as_str())
}
fn has_arg(&self, value: &str) -> bool {
self.args.iter().any(|a| a.as_str() == value)
}
fn arg_count(&self) -> usize {
self.args.len()
}
fn line(&self) -> usize {
self.span.start.line
}
fn column(&self) -> usize {
self.span.start.column
}
fn full_start_offset(&self) -> usize {
self.span.start.offset - self.leading_whitespace.len()
}
fn replace_with(&self, new_text: &str) -> Fix {
let start = self.full_start_offset();
let end = self.span.end.offset;
let fixed = format!("{}{}", self.leading_whitespace, new_text);
Fix::replace_range(start, end, &fixed)
}
fn delete_line(&self) -> Fix {
let start = self.full_start_offset();
let end = self.span.end.offset + self.trailing_whitespace.len();
let end = if let Some(ref comment) = self.trailing_comment {
comment.span.end.offset + comment.trailing_whitespace.len()
} else {
end
};
if start > 0 {
Fix::replace_range(start - 1, end, "")
} else {
Fix::replace_range(start, end + 1, "")
}
}
fn insert_after(&self, new_text: &str) -> Fix {
self.insert_after_many(&[new_text])
}
fn insert_after_many(&self, lines: &[&str]) -> Fix {
let indent = " ".repeat(self.span.start.column.saturating_sub(1));
let fix_text: String = lines
.iter()
.map(|line| format!("\n{}{}", indent, line))
.collect();
let insert_offset = self.span.end.offset;
Fix::replace_range(insert_offset, insert_offset, &fix_text)
}
fn insert_before(&self, new_text: &str) -> Fix {
self.insert_before_many(&[new_text])
}
fn insert_before_many(&self, lines: &[&str]) -> Fix {
let indent = " ".repeat(self.span.start.column.saturating_sub(1));
let fix_text: String = lines
.iter()
.map(|line| format!("{}{}\n", indent, line))
.collect();
let line_start_offset = self.span.start.offset - (self.span.start.column - 1);
Fix::replace_range(line_start_offset, line_start_offset, &fix_text)
}
}
impl<T: DirectiveExt + ?Sized> DirectiveExt for &T {
fn is(&self, name: &str) -> bool {
(**self).is(name)
}
fn first_arg(&self) -> Option<&str> {
(**self).first_arg()
}
fn first_arg_is(&self, value: &str) -> bool {
(**self).first_arg_is(value)
}
fn arg_at(&self, index: usize) -> Option<&str> {
(**self).arg_at(index)
}
fn last_arg(&self) -> Option<&str> {
(**self).last_arg()
}
fn has_arg(&self, value: &str) -> bool {
(**self).has_arg(value)
}
fn arg_count(&self) -> usize {
(**self).arg_count()
}
fn line(&self) -> usize {
(**self).line()
}
fn column(&self) -> usize {
(**self).column()
}
fn full_start_offset(&self) -> usize {
(**self).full_start_offset()
}
fn replace_with(&self, new_text: &str) -> Fix {
(**self).replace_with(new_text)
}
fn delete_line(&self) -> Fix {
(**self).delete_line()
}
fn insert_after(&self, new_text: &str) -> Fix {
(**self).insert_after(new_text)
}
fn insert_after_many(&self, lines: &[&str]) -> Fix {
(**self).insert_after_many(lines)
}
fn insert_before(&self, new_text: &str) -> Fix {
(**self).insert_before(new_text)
}
fn insert_before_many(&self, lines: &[&str]) -> Fix {
(**self).insert_before_many(lines)
}
}
impl DirectiveExt for Box<Directive> {
fn is(&self, name: &str) -> bool {
(**self).is(name)
}
fn first_arg(&self) -> Option<&str> {
(**self).first_arg()
}
fn first_arg_is(&self, value: &str) -> bool {
(**self).first_arg_is(value)
}
fn arg_at(&self, index: usize) -> Option<&str> {
(**self).arg_at(index)
}
fn last_arg(&self) -> Option<&str> {
(**self).last_arg()
}
fn has_arg(&self, value: &str) -> bool {
(**self).has_arg(value)
}
fn arg_count(&self) -> usize {
(**self).arg_count()
}
fn line(&self) -> usize {
(**self).line()
}
fn column(&self) -> usize {
(**self).column()
}
fn full_start_offset(&self) -> usize {
(**self).full_start_offset()
}
fn replace_with(&self, new_text: &str) -> Fix {
(**self).replace_with(new_text)
}
fn delete_line(&self) -> Fix {
(**self).delete_line()
}
fn insert_after(&self, new_text: &str) -> Fix {
(**self).insert_after(new_text)
}
fn insert_after_many(&self, lines: &[&str]) -> Fix {
(**self).insert_after_many(lines)
}
fn insert_before(&self, new_text: &str) -> Fix {
(**self).insert_before(new_text)
}
fn insert_before_many(&self, lines: &[&str]) -> Fix {
(**self).insert_before_many(lines)
}
}
pub trait ArgumentExt {
fn to_source(&self) -> String;
}
impl ArgumentExt for Argument {
fn to_source(&self) -> String {
match &self.value {
ArgumentValue::Literal(s) => s.clone(),
ArgumentValue::QuotedString(s) => format!("\"{}\"", s),
ArgumentValue::SingleQuotedString(s) => format!("'{}'", s),
ArgumentValue::Variable(s) => format!("${}", s),
}
}
}