Skip to content

Commit 73a886b

Browse files
committed
feat(linter): add require-namespace and readable-literal rules
- `require-namespace` triggers when a file contains definitions without a namespace declaration. - `readable-literal` enforces underscore separators in numeric literals (PHP 7.4+) with auto-fix support. closes #724 Signed-off-by: azjezz <[email protected]>
1 parent a330006 commit 73a886b

File tree

9 files changed

+534
-33
lines changed

9 files changed

+534
-33
lines changed

crates/linter/src/rule/best_practices/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub mod psl_randomness_functions;
2222
pub mod psl_regex_functions;
2323
pub mod psl_sleep_functions;
2424
pub mod psl_string_functions;
25+
pub mod require_namespace;
2526
pub mod use_compound_assignment;
2627
pub mod use_wp_functions;
2728
pub mod yoda_conditions;
@@ -50,6 +51,7 @@ pub use psl_randomness_functions::*;
5051
pub use psl_regex_functions::*;
5152
pub use psl_sleep_functions::*;
5253
pub use psl_string_functions::*;
54+
pub use require_namespace::*;
5355
pub use use_compound_assignment::*;
5456
pub use use_wp_functions::*;
5557
pub use yoda_conditions::*;
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
use indoc::indoc;
2+
use schemars::JsonSchema;
3+
use serde::Deserialize;
4+
use serde::Serialize;
5+
6+
use mago_reporting::Annotation;
7+
use mago_reporting::Issue;
8+
use mago_reporting::Level;
9+
use mago_span::HasSpan;
10+
use mago_syntax::ast::Node;
11+
use mago_syntax::ast::NodeKind;
12+
use mago_syntax::ast::Statement;
13+
14+
use crate::category::Category;
15+
use crate::context::LintContext;
16+
use crate::requirements::RuleRequirements;
17+
use crate::rule::Config;
18+
use crate::rule::LintRule;
19+
use crate::rule_meta::RuleMeta;
20+
use crate::settings::RuleSettings;
21+
22+
#[derive(Debug, Clone)]
23+
pub struct RequireNamespaceRule {
24+
meta: &'static RuleMeta,
25+
cfg: RequireNamespaceConfig,
26+
}
27+
28+
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize, JsonSchema)]
29+
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
30+
pub struct RequireNamespaceConfig {
31+
pub level: Level,
32+
}
33+
34+
impl Default for RequireNamespaceConfig {
35+
fn default() -> Self {
36+
Self { level: Level::Warning }
37+
}
38+
}
39+
40+
impl Config for RequireNamespaceConfig {
41+
fn level(&self) -> Level {
42+
self.level
43+
}
44+
45+
fn default_enabled() -> bool {
46+
false
47+
}
48+
}
49+
50+
impl LintRule for RequireNamespaceRule {
51+
type Config = RequireNamespaceConfig;
52+
53+
fn meta() -> &'static RuleMeta {
54+
const META: RuleMeta = RuleMeta {
55+
name: "Require Namespace",
56+
code: "require-namespace",
57+
description: indoc! {"
58+
Detects files that contain definitions (classes, interfaces, enums, traits, functions, or constants)
59+
but do not declare a namespace. Using namespaces helps avoid naming conflicts and improves code organization.
60+
"},
61+
good_example: indoc! {r#"
62+
<?php
63+
64+
namespace App;
65+
66+
class Foo {}
67+
"#},
68+
bad_example: indoc! {r#"
69+
<?php
70+
71+
class Foo {}
72+
"#},
73+
category: Category::BestPractices,
74+
requirements: RuleRequirements::None,
75+
};
76+
77+
&META
78+
}
79+
80+
fn targets() -> &'static [NodeKind] {
81+
const TARGETS: &[NodeKind] = &[NodeKind::Program];
82+
83+
TARGETS
84+
}
85+
86+
fn build(settings: &RuleSettings<Self::Config>) -> Self {
87+
Self { meta: Self::meta(), cfg: settings.config }
88+
}
89+
90+
fn check<'arena>(&self, ctx: &mut LintContext<'_, 'arena>, node: Node<'_, 'arena>) {
91+
let Node::Program(program) = node else {
92+
return;
93+
};
94+
95+
let mut has_namespace = false;
96+
let mut first_definition_span = None;
97+
98+
for statement in &program.statements {
99+
match statement {
100+
Statement::Namespace(_) => {
101+
has_namespace = true;
102+
break;
103+
}
104+
Statement::Class(class) => {
105+
if first_definition_span.is_none() {
106+
first_definition_span = Some(class.span());
107+
}
108+
}
109+
Statement::Interface(interface) => {
110+
if first_definition_span.is_none() {
111+
first_definition_span = Some(interface.span());
112+
}
113+
}
114+
Statement::Enum(e) => {
115+
if first_definition_span.is_none() {
116+
first_definition_span = Some(e.span());
117+
}
118+
}
119+
Statement::Trait(t) => {
120+
if first_definition_span.is_none() {
121+
first_definition_span = Some(t.span());
122+
}
123+
}
124+
Statement::Function(f) => {
125+
if first_definition_span.is_none() {
126+
first_definition_span = Some(f.span());
127+
}
128+
}
129+
Statement::Constant(c) => {
130+
if first_definition_span.is_none() {
131+
first_definition_span = Some(c.span());
132+
}
133+
}
134+
_ => {}
135+
}
136+
}
137+
138+
if has_namespace {
139+
return;
140+
}
141+
142+
let Some(definition_span) = first_definition_span else {
143+
return;
144+
};
145+
146+
let issue = Issue::new(self.cfg.level(), "File contains definitions without a namespace declaration.")
147+
.with_code(self.meta.code)
148+
.with_annotation(Annotation::primary(definition_span).with_message("Definition found without namespace"))
149+
.with_note("Using namespaces helps avoid naming conflicts and improves code organization.")
150+
.with_help("Add a namespace declaration at the top of the file.");
151+
152+
ctx.collector.report(issue);
153+
}
154+
}

crates/linter/src/rule/clarity/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod no_multi_assignments;
77
pub mod no_nested_ternary;
88
pub mod no_shorthand_ternary;
99
pub mod no_variable_variable;
10+
pub mod readable_literal;
1011
pub mod str_contains;
1112
pub mod str_starts_with;
1213
pub mod tagged_fixme;
@@ -22,6 +23,7 @@ pub use no_multi_assignments::*;
2223
pub use no_nested_ternary::*;
2324
pub use no_shorthand_ternary::*;
2425
pub use no_variable_variable::*;
26+
pub use readable_literal::*;
2527
pub use str_contains::*;
2628
pub use str_starts_with::*;
2729
pub use tagged_fixme::*;

0 commit comments

Comments
 (0)