Skip to content

Commit d41e64e

Browse files
authored
feat(linter): add invalid-open-tag correctness rule (#310)
closes #309 Signed-off-by: azjezz <[email protected]>
1 parent 138dc19 commit d41e64e

File tree

5 files changed

+212
-27
lines changed

5 files changed

+212
-27
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
use indoc::indoc;
2+
use serde::Deserialize;
3+
use serde::Serialize;
4+
5+
use mago_fixer::SafetyClassification;
6+
use mago_php_version::PHPVersionRange;
7+
use mago_reporting::Annotation;
8+
use mago_reporting::Issue;
9+
use mago_reporting::Level;
10+
use mago_span::HasSpan;
11+
use mago_syntax::ast::*;
12+
13+
use crate::category::Category;
14+
use crate::context::LintContext;
15+
use crate::integration::IntegrationSet;
16+
use crate::rule::Config;
17+
use crate::rule::LintRule;
18+
use crate::rule_meta::RuleMeta;
19+
use crate::settings::RuleSettings;
20+
21+
const INVALID_TAGS: &[&str] = &["<php?", "<ph?p", "<p?hp", "<ph?", "<p?"];
22+
23+
#[derive(Debug, Clone)]
24+
pub struct InvalidOpenTagRule {
25+
meta: &'static RuleMeta,
26+
cfg: InvalidOpenTagConfig,
27+
}
28+
29+
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
30+
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
31+
pub struct InvalidOpenTagConfig {
32+
pub level: Level,
33+
}
34+
35+
impl Default for InvalidOpenTagConfig {
36+
fn default() -> Self {
37+
Self { level: Level::Note }
38+
}
39+
}
40+
41+
impl Config for InvalidOpenTagConfig {
42+
fn level(&self) -> Level {
43+
self.level
44+
}
45+
}
46+
47+
impl LintRule for InvalidOpenTagRule {
48+
type Config = InvalidOpenTagConfig;
49+
50+
fn meta() -> &'static RuleMeta {
51+
const META: RuleMeta = RuleMeta {
52+
name: "Invalid Open Tag",
53+
code: "invalid-open-tag",
54+
description: indoc! {"
55+
Detects misspelled PHP opening tags like `<php?` instead of `<?php`.
56+
57+
A misspelled opening tag will cause the PHP interpreter to treat the
58+
following code as plain text, leading to the code being output directly
59+
to the browser instead of being executed. This can cause unexpected
60+
behavior and potential security vulnerabilities.
61+
"},
62+
good_example: indoc! {r#"
63+
<?php
64+
65+
echo 'Hello, world!';
66+
"#},
67+
bad_example: indoc! {r#"
68+
<php?
69+
70+
echo 'Hello, world!';
71+
"#},
72+
category: Category::Correctness,
73+
php: PHPVersionRange::any(),
74+
requires: IntegrationSet::empty(),
75+
};
76+
77+
&META
78+
}
79+
80+
fn targets() -> &'static [NodeKind] {
81+
const TARGETS: &[NodeKind] = &[NodeKind::Inline];
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<'ast, 'arena>(&self, ctx: &mut LintContext<'_, 'arena>, node: Node<'ast, 'arena>) {
91+
let Node::Inline(inline_stmt) = node else {
92+
return;
93+
};
94+
95+
let content = inline_stmt.value;
96+
let trimmed_content = content.trim_start();
97+
98+
for &invalid_tag in INVALID_TAGS {
99+
let invalid_tag_len = invalid_tag.len();
100+
if trimmed_content.len() < invalid_tag_len {
101+
continue;
102+
}
103+
104+
let prefix_to_check = &trimmed_content[..invalid_tag_len];
105+
106+
if prefix_to_check.eq_ignore_ascii_case(invalid_tag) {
107+
let start_offset = content.len() - trimmed_content.len();
108+
let invalid_tag_span = inline_stmt.span().subspan(start_offset as u32, invalid_tag_len as u32);
109+
110+
let issue = Issue::new(self.cfg.level(), format!("Misspelled PHP opening tag `{}`.", invalid_tag))
111+
.with_code(self.meta.code)
112+
.with_annotation(
113+
Annotation::primary(invalid_tag_span).with_message("This looks like a typo for `<?php`."),
114+
)
115+
.with_note("Code following a misspelled tag will be treated as plain text and output directly.")
116+
.with_help("Replace this with the correct `<?php` opening tag.");
117+
118+
ctx.collector.propose(issue, |plan| {
119+
plan.replace(invalid_tag_span.to_range(), "<?php", SafetyClassification::PotentiallyUnsafe);
120+
});
121+
122+
break;
123+
}
124+
}
125+
}
126+
}

crates/linter/src/rule/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub mod function_name;
3333
pub mod halstead;
3434
pub mod identity_comparison;
3535
pub mod interface_name;
36+
pub mod invalid_open_tag;
3637
pub mod kan_defect;
3738
pub mod literal_named_argument;
3839
pub mod loop_does_not_iterate;
@@ -137,6 +138,7 @@ pub use function_name::*;
137138
pub use halstead::*;
138139
pub use identity_comparison::*;
139140
pub use interface_name::*;
141+
pub use invalid_open_tag::*;
140142
pub use kan_defect::*;
141143
pub use literal_named_argument::*;
142144
pub use loop_does_not_iterate::*;
@@ -378,8 +380,9 @@ define_rules! {
378380
NoAssignInCondition(no_assign_in_condition @ NoAssignInConditionRule),
379381
NoAliasFunction(no_alias_function @ NoAliasFunctionRule),
380382
LowercaseTypeHint(lowercase_type_hint @ LowercaseTypeHintRule),
381-
InterfaceName(interface_name @ InterfaceNameRule),
382383
IdentityComparison(identity_comparison @ IdentityComparisonRule),
384+
InterfaceName(interface_name @ InterfaceNameRule),
385+
InvalidOpenTag(invalid_open_tag @ InvalidOpenTagRule),
383386
FunctionName(function_name @ FunctionNameRule),
384387
ExplicitOctal(explicit_octal @ ExplicitOctalRule),
385388
ExplicitNullableParam(explicit_nullable_param @ ExplicitNullableParamRule),

crates/linter/src/rule/strict_types.rs

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -92,38 +92,54 @@ impl LintRule for StrictTypesRule {
9292
};
9393

9494
let mut found = false;
95+
let mut has_useful_statements = false;
9596
for statement in program.statements.iter() {
96-
if let Statement::Declare(declare) = statement {
97-
for item in declare.items.iter() {
98-
if item.name.value != STRICT_TYPES_DIRECTIVE {
99-
continue;
100-
}
97+
let declare = match statement {
98+
Statement::Declare(declare) => declare,
99+
_ => {
100+
has_useful_statements |= !matches!(
101+
statement,
102+
Statement::OpeningTag(_) | Statement::ClosingTag(_) | Statement::Inline(_) | Statement::Noop(_)
103+
);
104+
105+
break;
106+
}
107+
};
101108

102-
match &item.value {
103-
Expression::Literal(Literal::Integer(integer)) => {
104-
if integer.value == Some(0) && !self.cfg.allow_disabling {
105-
let issue = Issue::new(self.cfg.level(), "The `strict_types` directive is disabled.")
106-
.with_code(self.meta.code)
107-
.with_annotation(
108-
Annotation::primary(item.span())
109-
.with_message("The `strict_types` is disabled here"),
110-
)
111-
.with_note("Disabling `strict_types` can lead to type safety issues.")
112-
.with_help("Consider setting `strict_types` to `1` to enforce strict typing.");
113-
114-
ctx.collector.report(issue);
115-
}
116-
}
117-
_ => {
118-
// ignore other values, as they will be caught by the semantics checker
109+
for item in declare.items.iter() {
110+
if item.name.value != STRICT_TYPES_DIRECTIVE {
111+
continue;
112+
}
113+
114+
match &item.value {
115+
Expression::Literal(Literal::Integer(integer)) => {
116+
if integer.value == Some(0) && !self.cfg.allow_disabling {
117+
let issue = Issue::new(self.cfg.level(), "The `strict_types` directive is disabled.")
118+
.with_code(self.meta.code)
119+
.with_annotation(
120+
Annotation::primary(item.span())
121+
.with_message("The `strict_types` is disabled here"),
122+
)
123+
.with_note("Disabling `strict_types` can lead to type safety issues.")
124+
.with_help("Consider setting `strict_types` to `1` to enforce strict typing.");
125+
126+
ctx.collector.report(issue);
119127
}
120-
};
128+
}
129+
_ => {
130+
// ignore other values, as they will be caught by the semantics checker
131+
}
132+
};
121133

122-
found = true;
123-
}
134+
found = true;
124135
}
125136
}
126137

138+
if !has_useful_statements {
139+
// empty file or file with only opening/closing tags, inline HTML, or no-op statements
140+
return;
141+
}
142+
127143
if !found {
128144
let issue = Issue::new(
129145
self.cfg.level(),

crates/linter/src/settings.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,9 @@ pub struct RulesSettings {
9090
pub no_assign_in_condition: RuleSettings<NoAssignInConditionConfig>,
9191
pub no_alias_function: RuleSettings<NoAliasFunctionConfig>,
9292
pub lowercase_type_hint: RuleSettings<LowercaseTypeHintConfig>,
93-
pub interface_name: RuleSettings<InterfaceNameConfig>,
9493
pub identity_comparison: RuleSettings<IdentityComparisonConfig>,
94+
pub interface_name: RuleSettings<InterfaceNameConfig>,
95+
pub invalid_open_tag: RuleSettings<InvalidOpenTagConfig>,
9596
pub function_name: RuleSettings<FunctionNameConfig>,
9697
pub explicit_nullable_param: RuleSettings<ExplicitNullableParamConfig>,
9798
pub explicit_octal: RuleSettings<ExplicitOctalConfig>,

docs/tools/linter/rules/correctness.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This document details the rules available in the `Correctness` category.
1212
| :--- | :---------- |
1313
| Assert Description | [`assert-description`](#assert-description) |
1414
| Identity Comparison | [`identity-comparison`](#identity-comparison) |
15+
| Invalid Open Tag | [`invalid-open-tag`](#invalid-open-tag) |
1516
| No Assign In Condition | [`no-assign-in-condition`](#no-assign-in-condition) |
1617
| No Boolean Literal Comparison | [`no-boolean-literal-comparison`](#no-boolean-literal-comparison) |
1718
| No Empty Catch Clause | [`no-empty-catch-clause`](#no-empty-catch-clause) |
@@ -96,6 +97,44 @@ if ($a == $b) {
9697

9798
---
9899

100+
## <a id="invalid-open-tag"></a>`invalid-open-tag`
101+
102+
Detects misspelled PHP opening tags like `<php?` instead of `<?php`.
103+
104+
A misspelled opening tag will cause the PHP interpreter to treat the
105+
following code as plain text, leading to the code being output directly
106+
to the browser instead of being executed. This can cause unexpected
107+
behavior and potential security vulnerabilities.
108+
109+
110+
111+
### Configuration
112+
113+
| Option | Type | Default |
114+
| :--- | :--- | :--- |
115+
| `enabled` | `boolean` | `true` |
116+
| `level` | `string` | `"note"` |
117+
118+
### Examples
119+
120+
#### Correct Code
121+
122+
```php
123+
<?php
124+
125+
echo 'Hello, world!';
126+
```
127+
128+
#### Incorrect Code
129+
130+
```php
131+
<php?
132+
133+
echo 'Hello, world!';
134+
```
135+
136+
---
137+
99138
## <a id="no-assign-in-condition"></a>`no-assign-in-condition`
100139

101140
Detects assignments in conditions which can lead to unexpected behavior and make the code harder

0 commit comments

Comments
 (0)