Skip to content

Commit

Permalink
feat(linter): add phpunit plugin (#24)
Browse files Browse the repository at this point in the history
Signed-off-by: azjezz <[email protected]>
  • Loading branch information
azjezz authored Dec 10, 2024
1 parent d4b16e5 commit f7a1b0c
Show file tree
Hide file tree
Showing 7 changed files with 979 additions and 0 deletions.
1 change: 1 addition & 0 deletions crates/linter/src/plugin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod best_practices;
pub mod comment;
pub mod consistency;
pub mod naming;
pub mod phpunit;
pub mod redundancy;
pub mod safety;
pub mod strictness;
Expand Down
24 changes: 24 additions & 0 deletions crates/linter/src/plugin/phpunit/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use crate::plugin::phpunit::rules::consistency::assertions_style::AssertionsStyleRule;
use crate::plugin::phpunit::rules::strictness::strict_assertions::StrictAssertionsRule;

use crate::plugin::Plugin;
use crate::rule::Rule;

pub mod rules;

#[derive(Debug)]
pub struct PHPUnitPlugin;

impl Plugin for PHPUnitPlugin {
fn get_name(&self) -> &'static str {
"phpunit"
}

fn is_enabled_by_default(&self) -> bool {
false
}

fn get_rules(&self) -> Vec<Box<dyn Rule>> {
vec![Box::new(AssertionsStyleRule), Box::new(StrictAssertionsRule)]
}
}
103 changes: 103 additions & 0 deletions crates/linter/src/plugin/phpunit/rules/consistency/assertions_style.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use mago_ast::*;
use mago_fixer::SafetyClassification;
use mago_reporting::*;
use mago_span::HasSpan;
use mago_walker::Walker;

use crate::context::LintContext;
use crate::plugin::phpunit::rules::utils::find_testing_and_assertions_methods;
use crate::plugin::phpunit::rules::utils::MethodReference;
use crate::rule::Rule;

const STATIC_STYLES: &str = "static";
const SELF_STYLES: &str = "self";
const THIS_STYLES: &str = "this";

const STYLES: [&str; 3] = [STATIC_STYLES, SELF_STYLES, THIS_STYLES];

#[derive(Clone, Debug)]
pub struct AssertionsStyleRule;

impl Rule for AssertionsStyleRule {
fn get_name(&self) -> &'static str {
"assertions-style"
}

fn get_default_level(&self) -> Option<Level> {
Some(Level::Warning)
}
}

impl<'a> Walker<LintContext<'a>> for AssertionsStyleRule {
fn walk_in_method(&self, method: &Method, context: &mut LintContext<'a>) {
let name = context.lookup(&method.name.value);
if !name.starts_with("test") || name.chars().nth(4).is_none_or(|c| c != '_' && !c.is_uppercase()) {
return;
}

let desired_style = context
.option("style")
.and_then(|o| o.as_str())
.filter(|s| STYLES.contains(&s.to_ascii_lowercase().as_str()))
.unwrap_or(STATIC_STYLES)
.to_string();

let desired_syntax = match desired_style.as_str() {
STATIC_STYLES => "static::",
SELF_STYLES => "self::",
THIS_STYLES => "$this->",
_ => unreachable!(),
};

for reference in find_testing_and_assertions_methods(method, context) {
let (to_replace, current_style) = match reference {
MethodReference::MethodCall(c) => (c.object.span().join(c.arrow), THIS_STYLES),
MethodReference::MethodClosureCreation(c) => (c.object.span().join(c.arrow), THIS_STYLES),
MethodReference::StaticMethodClosureCreation(StaticMethodClosureCreation {
class,
double_colon,
..
}) => match class {
Expression::Static(_) => (class.span().join(*double_colon), STATIC_STYLES),
Expression::Self_(_) => (class.span().join(*double_colon), SELF_STYLES),
_ => continue,
},
MethodReference::StaticMethodCall(StaticMethodCall { class, double_colon, .. }) => match class.as_ref()
{
Expression::Static(_) => (class.span().join(*double_colon), STATIC_STYLES),
Expression::Self_(_) => (class.span().join(*double_colon), SELF_STYLES),
_ => continue,
},
};

if current_style == desired_style {
continue;
}

let current_syntax = match current_style {
STATIC_STYLES => "static::",
SELF_STYLES => "self::",
THIS_STYLES => "$this->",
_ => unreachable!(),
};

let issue = Issue::new(context.level(), "inconsistent assertions style")
.with_annotations([Annotation::primary(reference.span()).with_message(format!(
"assertion style mismatch: expected `{}` style but found `{}` style.",
desired_style, current_style
))])
.with_help(format!(
"use `{}` instead of `{}` to conform to the `{}` style.",
desired_syntax, current_syntax, desired_style,
));

context.report_with_fix(issue, |plan| {
plan.replace(
to_replace.to_range(),
desired_syntax.to_string(),
SafetyClassification::PotentiallyUnsafe,
);
});
}
}
}
9 changes: 9 additions & 0 deletions crates/linter/src/plugin/phpunit/rules/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
mod utils;

pub mod consistency {
pub mod assertions_style;
}

pub mod strictness {
pub mod strict_assertions;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use mago_ast::*;
use mago_fixer::SafetyClassification;
use mago_reporting::*;
use mago_span::HasSpan;
use mago_walker::Walker;

use crate::context::LintContext;
use crate::plugin::phpunit::rules::utils::find_assertions_methods;
use crate::rule::Rule;

const NON_STRICT_ASSERTIONS: [&str; 4] =
["assertAttributeEquals", "assertAttributeNotEquals", "assertEquals", "assertNotEquals"];

/// A PHPUnit rule that enforces the use of strict assertions.
#[derive(Clone, Debug)]
pub struct StrictAssertionsRule;

impl Rule for StrictAssertionsRule {
fn get_name(&self) -> &'static str {
"strict-assertions"
}

fn get_default_level(&self) -> Option<Level> {
Some(Level::Warning)
}
}

impl<'a> Walker<LintContext<'a>> for StrictAssertionsRule {
fn walk_in_method(&self, method: &Method, context: &mut LintContext<'a>) {
let name = context.lookup(&method.name.value);
if !name.starts_with("test") || name.chars().nth(4).is_none_or(|c| c != '_' && !c.is_uppercase()) {
return;
}

for reference in find_assertions_methods(method, context) {
let ClassLikeMemberSelector::Identifier(identifier) = reference.get_selector() else {
continue;
};

let name = context.lookup(&identifier.value);
if NON_STRICT_ASSERTIONS.contains(&name) {
let strict_name = name.replacen("Equals", "Same", 1);

let issue = Issue::new(context.level(), "use strict assertions in PHPUnit tests")
.with_annotations([Annotation::primary(reference.span())
.with_message(format!("replace `{}` with `{}`", name, strict_name))])
.with_help(format!(
"replace `{}` with `{}` to enforce strict comparisons in your tests.",
name, strict_name
));

context.report_with_fix(issue, |plan| {
plan.replace(
reference.get_selector().span().to_range(),
strict_name,
SafetyClassification::PotentiallyUnsafe,
);
});
}
}
}
}
Loading

0 comments on commit f7a1b0c

Please sign in to comment.