-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement scope rules, add config system, print all reports
- Loading branch information
Matthias Zaunseder
committed
Feb 6, 2024
1 parent
361bc1a
commit 3d52fda
Showing
7 changed files
with
471 additions
and
104 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,8 @@ | ||
{ | ||
"rules": { | ||
"scope-enum": ["error", "always", ["foo", "bar", "baz"]] | ||
"scope-empty": ["error", "never"], | ||
"scope-enum": ["error", "always", ["foo", "bar", "baz"]], | ||
"scope-max-length": ["error", 2], | ||
"scope-case": ["error", "always", "lower-case"] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,117 +1,50 @@ | ||
mod parser; | ||
mod rules; | ||
|
||
use config::Config; | ||
use miette::{miette, LabeledSpan, Report, Result}; | ||
use parser::{parse_commit, Commit}; | ||
use serde::Deserialize; | ||
use std::process::ExitCode; | ||
|
||
fn rule_scope_enum(commit: &Commit, opts: &ScopeEnumOpts) -> Option<Report> { | ||
let severity = &opts.0; | ||
let condition = &opts.1; | ||
let scopes = &opts.2; | ||
use miette::GraphicalReportHandler; | ||
use parser::parse_commit; | ||
|
||
if severity == &Severity::Off { | ||
return None; | ||
} | ||
|
||
if let Some(scope) = &commit.scope { | ||
let is_in_scopes = scopes.contains(&scope.to_string()); | ||
let is_valid = match condition { | ||
Condition::Never => !is_in_scopes, | ||
Condition::Always => is_in_scopes, | ||
}; | ||
if !is_valid { | ||
return Some( | ||
miette!( | ||
severity = match severity { | ||
Severity::Warning => miette::Severity::Warning, | ||
Severity::Error => miette::Severity::Error, | ||
Severity::Off => miette::Severity::Advice, | ||
}, | ||
labels = vec![LabeledSpan::at( | ||
scope.start()..scope.end(), | ||
"not allowed scope" | ||
),], | ||
help = String::from("scope must") + match condition { | ||
Condition::Never => " not", | ||
Condition::Always => "", | ||
} + " be one of " + &scopes.join(", "), | ||
code = "rule/scope-enum", | ||
url = "https://example.com", | ||
"Scope not allowed", | ||
) | ||
.with_source_code(commit.raw.clone()), | ||
); | ||
} | ||
} | ||
|
||
None | ||
} | ||
|
||
/// Severity of the rule | ||
#[derive(Debug, Deserialize, PartialEq)] | ||
enum Severity { | ||
/// Turn off the rule | ||
#[serde(rename = "off")] | ||
Off, | ||
/// Warn about the violation of a rule | ||
#[serde(rename = "warning")] | ||
Warning, | ||
/// Error about the violation of a rule | ||
#[serde(rename = "error")] | ||
Error, | ||
} | ||
|
||
/// When the rule should be applied | ||
#[derive(Debug, Deserialize)] | ||
enum Condition { | ||
/// The options should "never" be found (e.g. in a list of disallowed values) | ||
#[serde(rename = "never")] | ||
Never, | ||
/// The options should "always" be found (e.g. in a list of allowed values) | ||
#[serde(rename = "always")] | ||
Always, | ||
} | ||
|
||
/// Options for the scope-enum rule | ||
type ScopeEnumOpts = (Severity, Condition, Vec<String>); | ||
|
||
/// Config all the rules | ||
#[derive(Debug, Deserialize)] | ||
struct RulesDetails { | ||
#[serde(rename = "scope-enum")] | ||
scope_enum: ScopeEnumOpts, | ||
} | ||
|
||
/// Config | ||
#[derive(Debug, Deserialize)] | ||
struct RulesConfig { | ||
rules: RulesDetails, | ||
} | ||
|
||
fn main() -> Result<()> { | ||
fn main() -> ExitCode { | ||
let commit_message = | ||
"feat(nice): add cool feature\n\nsome body\n\nsecond body line\n\nsome footer"; | ||
|
||
let commit = parse_commit(&commit_message); | ||
println!("{:#?}", commit); | ||
|
||
let settings = Config::builder() | ||
// Source can be `commitlint.config.toml` or `commitlint.config.json | ||
.add_source(config::File::with_name("src/commitlint.config")) | ||
// Add in settings from the environment (with a prefix of APP) | ||
// Eg.. `COMMITLINT_DEBUG=1 ./target/app` would set the `debug` key | ||
.add_source(config::Environment::with_prefix("COMMITLINT")) | ||
.build() | ||
.unwrap(); | ||
let lint_result = rules::run(&commit); | ||
let report_handler = GraphicalReportHandler::new(); | ||
|
||
if lint_result.has_warnings() { | ||
let mut out = String::new(); | ||
lint_result.warnings().unwrap().iter().for_each(|report| { | ||
out.push('\n'); | ||
let _ = report_handler.render_report(&mut out, report.as_ref()); | ||
}); | ||
|
||
println!("{}", out); | ||
} | ||
|
||
if lint_result.has_errors() { | ||
let mut out = String::new(); | ||
lint_result.errors().unwrap().iter().for_each(|report| { | ||
out.push('\n'); | ||
let _ = report_handler.render_report(&mut out, report.as_ref()); | ||
}); | ||
|
||
println!("{}", out); | ||
} | ||
|
||
// Print out our settings | ||
let config: RulesConfig = settings.try_deserialize::<RulesConfig>().unwrap(); | ||
println!("{:?}", config); | ||
println!( | ||
"You have {} warnings and {} errors", | ||
lint_result.warnings_len(), | ||
lint_result.errors_len() | ||
); | ||
|
||
if let Some(report) = rule_scope_enum(&commit, &config.rules.scope_enum) { | ||
return Err(report); | ||
if lint_result.has_errors() { | ||
return ExitCode::FAILURE; | ||
} | ||
|
||
Ok(()) | ||
ExitCode::SUCCESS | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
use config::Config; | ||
use serde::Deserialize; | ||
|
||
use crate::parser::Commit; | ||
|
||
pub mod scope_empty; | ||
pub mod scope_enum; | ||
pub mod scope_max_length; | ||
|
||
pub(crate) trait Rule { | ||
fn run(&self, commit: &Commit) -> Option<miette::Report>; | ||
} | ||
|
||
/// Severity of the rule | ||
#[derive(Debug, Deserialize, PartialEq)] | ||
pub(crate) enum Severity { | ||
/// Turn off the rule | ||
#[serde(rename = "off")] | ||
Off, | ||
/// Warn about the violation of a rule | ||
#[serde(rename = "warning")] | ||
Warning, | ||
/// Error about the violation of a rule | ||
#[serde(rename = "error")] | ||
Error, | ||
} | ||
|
||
/// When the rule should be applied | ||
#[derive(Debug, Deserialize)] | ||
pub(crate) enum Condition { | ||
/// The options should "never" be found (e.g. in a list of disallowed values) | ||
#[serde(rename = "never")] | ||
Never, | ||
/// The options should "always" be found (e.g. in a list of allowed values) | ||
#[serde(rename = "always")] | ||
Always, | ||
} | ||
|
||
/// Possible target cases for the rule (e.g. subject must start with a capital letter: `TargetCase::Sentence`) | ||
#[derive(Debug, Deserialize)] | ||
enum TargetCase { | ||
/// Lower case (e.g. `sometext`) | ||
#[serde(rename = "lower-case")] | ||
Lower, | ||
/// Upper case (e.g. `SOMETEXT`) | ||
#[serde(rename = "upper-case")] | ||
Upper, | ||
/// Pascal case (e.g. `SomeText`) | ||
#[serde(rename = "pascal-case")] | ||
Pascal, | ||
/// Camel case (e.g. `someText`) | ||
#[serde(rename = "camel-case")] | ||
Camel, | ||
/// Kebab case (e.g. `some-text`) | ||
#[serde(rename = "kebab-case")] | ||
Kebab, | ||
/// Snake case (e.g. `some_text`) | ||
#[serde(rename = "snake-case")] | ||
Snake, | ||
/// Start case (e.g. `Some Text`) | ||
#[serde(rename = "start-case")] | ||
Start, | ||
/// Sentence case (e.g. `Some text`) | ||
#[serde(rename = "sentence-case")] | ||
Sentence, | ||
} | ||
|
||
/// Options for all rules without options | ||
#[derive(Debug, Deserialize)] | ||
pub(crate) struct NoOpts(Severity, Condition); | ||
/// Options for all enum rules | ||
type EnumOpts = (Severity, Condition, Vec<String>); | ||
/// Options for all length rules | ||
type LengthOpts = (Severity, usize); | ||
/// Options for all case rules | ||
type CaseOpts = (Severity, Condition, TargetCase); | ||
|
||
/// Config all the rules | ||
#[derive(Debug, Deserialize)] | ||
struct RulesDetails { | ||
#[serde(rename = "scope-empty")] | ||
scope_empty: NoOpts, | ||
#[serde(rename = "scope-enum")] | ||
scope_enum: EnumOpts, | ||
#[serde(rename = "scope-max-length")] | ||
scope_max_length: LengthOpts, | ||
#[serde(rename = "scope-case")] | ||
scope_case: CaseOpts, | ||
} | ||
|
||
/// Config | ||
#[derive(Debug, Deserialize)] | ||
struct RulesConfig { | ||
rules: RulesDetails, | ||
} | ||
|
||
pub struct LintResult { | ||
errors: Option<Vec<miette::Report>>, | ||
warnings: Option<Vec<miette::Report>>, | ||
} | ||
|
||
impl LintResult { | ||
pub fn errors(&self) -> Option<&Vec<miette::Report>> { | ||
self.errors.as_ref() | ||
} | ||
|
||
pub fn errors_len(&self) -> usize { | ||
match self.errors() { | ||
None => return 0, | ||
Some(errors) => errors.len(), | ||
} | ||
} | ||
|
||
pub fn has_errors(&self) -> bool { | ||
self.errors.is_some() && !self.errors().unwrap().is_empty() | ||
} | ||
|
||
pub fn warnings(&self) -> Option<&Vec<miette::Report>> { | ||
self.warnings.as_ref() | ||
} | ||
|
||
pub fn warnings_len(&self) -> usize { | ||
match self.warnings() { | ||
None => return 0, | ||
Some(warnings) => warnings.len(), | ||
} | ||
} | ||
|
||
pub fn has_warnings(&self) -> bool { | ||
self.warnings.is_some() && !self.warnings().unwrap().is_empty() | ||
} | ||
} | ||
|
||
pub fn run(commit: &Commit) -> LintResult { | ||
let settings = Config::builder() | ||
// Source can be `commitlint.config.toml` or `commitlint.config.json`` | ||
.add_source(config::File::with_name("src/commitlint.config")) | ||
.build() | ||
.unwrap(); | ||
|
||
// Print out our settings | ||
let config: RulesConfig = settings.try_deserialize::<RulesConfig>().unwrap(); | ||
println!("{:?}", config); | ||
|
||
// create list of rules to iterate over them | ||
let rules: Vec<Box<dyn Rule>> = vec![ | ||
Box::new(scope_empty::ScopeEmptyRule { | ||
opts: config.rules.scope_empty, | ||
}), | ||
Box::new(scope_enum::ScopeEnumRule { | ||
opts: config.rules.scope_enum, | ||
}), | ||
Box::new(scope_max_length::ScopeMaxLengthRule { | ||
opts: config.rules.scope_max_length, | ||
}), | ||
]; | ||
|
||
// iterate over all rules and run them and return all found errors and warnings | ||
let mut lint_result = LintResult { | ||
errors: None, | ||
warnings: None, | ||
}; | ||
for rule in rules { | ||
if let Some(report) = rule.run(&commit) { | ||
match report.severity() { | ||
Some(miette::Severity::Error) => { | ||
if lint_result.errors.is_none() { | ||
lint_result.errors = Some(vec![]); | ||
} | ||
lint_result.errors.as_mut().unwrap().push(report); | ||
} | ||
Some(miette::Severity::Warning) => { | ||
if lint_result.warnings.is_none() { | ||
lint_result.warnings = Some(vec![]); | ||
} | ||
lint_result.warnings.as_mut().unwrap().push(report); | ||
} | ||
_ => {} | ||
} | ||
} | ||
} | ||
|
||
return lint_result; | ||
} |
Oops, something went wrong.