Skip to content

Commit 03d3d4d

Browse files
committed
feat(linter): add angular/prefer-standalone
1 parent 669738d commit 03d3d4d

File tree

4 files changed

+355
-0
lines changed

4 files changed

+355
-0
lines changed

crates/oxc_linter/src/rules.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,7 @@ pub(crate) mod vue {
664664
}
665665

666666
pub(crate) mod angular {
667+
pub mod prefer_standalone;
667668
}
668669

669670
oxc_macros::declare_all_lint_rules! {
@@ -1278,4 +1279,5 @@ oxc_macros::declare_all_lint_rules! {
12781279
vue::require_typed_ref,
12791280
vue::valid_define_emits,
12801281
vue::valid_define_props,
1282+
angular::prefer_standalone,
12811283
}
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
use oxc_ast::AstKind;
2+
use oxc_ast::ast::{
3+
CallExpression, Class, Expression, ObjectExpression, ObjectProperty, ObjectPropertyKind,
4+
PropertyKey,
5+
};
6+
use oxc_diagnostics::OxcDiagnostic;
7+
use oxc_macros::declare_oxc_lint;
8+
use oxc_span::{GetSpan, Span};
9+
10+
use crate::{AstNode, context::LintContext, rule::Rule};
11+
12+
fn prefer_standalone_diagnostic(span: Span) -> OxcDiagnostic {
13+
OxcDiagnostic::warn("Prefer standalone components.").with_label(span)
14+
}
15+
16+
#[derive(Debug, Default, Clone)]
17+
pub struct PreferStandalone;
18+
19+
// See <https://github.com/oxc-project/oxc/issues/6050> for documentation details.
20+
declare_oxc_lint!(
21+
/// ### What it does
22+
///
23+
/// Components, Directives and Pipes should not opt out of standalone.
24+
///
25+
/// ### Why is this bad?
26+
///
27+
/// Since Angular 19, components, directives and pipes have been standalone by default.
28+
/// It is the recommended way to create them.
29+
/// Therefore, you should not opt out of standalone.
30+
///
31+
/// ### Examples
32+
///
33+
/// Examples of **incorrect** code for this rule:
34+
/// ```js
35+
/// @Component({ standalone: false })
36+
/// class TestComponent {}
37+
///
38+
/// @Directive({ standalone: false })
39+
/// class TestDirective {}
40+
/// ```
41+
///
42+
/// Examples of **correct** code for this rule:
43+
/// ```js
44+
/// @Component()
45+
/// class TestComponent {}
46+
///
47+
/// @Component({ standalone: true })
48+
/// class TestComponent {}
49+
///
50+
/// @Directive()
51+
/// class TestDirective {}
52+
///
53+
/// @Directive({ standalone: true })
54+
/// class TestDirective {}
55+
/// ```
56+
PreferStandalone,
57+
angular,
58+
style,
59+
suggestion
60+
);
61+
62+
impl Rule for PreferStandalone {
63+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
64+
let AstKind::Class(class) = node.kind() else {
65+
return;
66+
};
67+
let Some(call_expression) = get_relevant_decorator_call_expression(class) else { return };
68+
let Some(first_arg) = call_expression.arguments.first() else {
69+
return;
70+
};
71+
let Expression::ObjectExpression(obj_expression) = &first_arg.to_expression() else {
72+
return;
73+
};
74+
let Some(prop) = get_standalone_property(obj_expression) else { return };
75+
let Expression::BooleanLiteral(bool_value) = &prop.value else {
76+
return;
77+
};
78+
79+
if !bool_value.value {
80+
ctx.diagnostic_with_suggestion(prefer_standalone_diagnostic(prop.span()), |fixer| {
81+
fixer.replace(prop.span(), "")
82+
});
83+
}
84+
}
85+
}
86+
87+
fn get_relevant_decorator_call_expression<'a>(
88+
class: &'a Class<'a>,
89+
) -> Option<&'a CallExpression<'a>> {
90+
for decorator in &class.decorators {
91+
let Expression::CallExpression(call_expr) = &decorator.expression else {
92+
continue;
93+
};
94+
let Some(callee_identifier) = call_expr.callee.get_identifier_reference() else { continue };
95+
if ["Component", "Directive", "Pipe"].contains(&callee_identifier.name.as_str()) {
96+
return Some(call_expr);
97+
}
98+
}
99+
None
100+
}
101+
102+
fn get_standalone_property<'a>(
103+
decorator_object_expression: &'a ObjectExpression<'a>,
104+
) -> Option<&'a ObjectProperty<'a>> {
105+
for property in &decorator_object_expression.properties {
106+
let ObjectPropertyKind::ObjectProperty(prop) = &property else {
107+
continue;
108+
};
109+
let PropertyKey::StaticIdentifier(ident) = &prop.key else {
110+
continue;
111+
};
112+
if ident.name == "standalone" {
113+
return Some(prop);
114+
}
115+
}
116+
None
117+
}
118+
119+
#[test]
120+
fn test() {
121+
use crate::tester::Tester;
122+
123+
let pass = vec![
124+
"@Component({})
125+
class Test {}",
126+
"@Component({
127+
standalone: true,
128+
})
129+
class Test {}",
130+
"@Component({
131+
selector: 'test-selector'
132+
})
133+
class Test {}",
134+
"@Component({
135+
standalone: true,
136+
selector: 'test-selector'
137+
})
138+
class Test {}",
139+
"@Component({
140+
selector: 'test-selector',
141+
template: '<div></div>',
142+
styleUrls: ['./test.css']
143+
})
144+
class Test {}",
145+
"@Component({
146+
selector: 'test-selector',
147+
standalone: true,
148+
template: '<div></div>',
149+
styleUrls: ['./test.css']
150+
})
151+
class Test {}",
152+
"@Directive({})
153+
class Test {}",
154+
"@Directive({
155+
standalone: true,
156+
})
157+
class Test {}",
158+
"@Directive({
159+
selector: 'test-selector'
160+
})
161+
class Test {}",
162+
"@Directive({
163+
standalone: true,
164+
selector: 'test-selector'
165+
})
166+
class Test {}",
167+
"@Directive({
168+
selector: 'test-selector',
169+
providers: []
170+
})
171+
class Test {}",
172+
"@Directive({
173+
selector: 'test-selector',
174+
standalone: true,
175+
providers: []
176+
})
177+
class Test {}",
178+
"@Directive()
179+
abstract class Test {}",
180+
"@Pipe({})
181+
class Test {}",
182+
"@Pipe({
183+
standalone: true,
184+
})
185+
class Test {}",
186+
"@Pipe({
187+
name: 'test-pipe'
188+
})
189+
class Test {}",
190+
"@Pipe({
191+
standalone: true,
192+
name: 'test-pipe'
193+
})
194+
class Test {}",
195+
"@Pipe({
196+
name: 'my-pipe',
197+
pure: true
198+
})
199+
class Test {}",
200+
"@Pipe({
201+
name: 'my-pipe',
202+
standalone: true,
203+
pure: true
204+
})
205+
class Test {}",
206+
];
207+
208+
let fail = vec![
209+
"@Component({ standalone: false })
210+
class Test {}",
211+
"@Component({
212+
standalone: false,
213+
template: '<div></div>'
214+
})
215+
class Test {}",
216+
"@Directive({ standalone: false })
217+
class Test {}",
218+
"@Directive({
219+
standalone: false,
220+
selector: 'x-selector'
221+
})
222+
class Test {}",
223+
"@Pipe({ standalone: false })
224+
class Test {}",
225+
"@Pipe({
226+
standalone: false,
227+
name: 'pipe-name'
228+
})
229+
class Test {}",
230+
];
231+
232+
let fix = vec![
233+
(
234+
"@Component({ standalone: false })
235+
class Test {}",
236+
"@Component({ })
237+
class Test {}",
238+
),
239+
(
240+
"@Component({
241+
standalone: false,
242+
template: '<div></div>'
243+
})
244+
class Test {}",
245+
"@Component({
246+
,
247+
template: '<div></div>'
248+
})
249+
class Test {}",
250+
),
251+
(
252+
"@Directive({ standalone: false })
253+
class Test {}",
254+
"@Directive({ })
255+
class Test {}",
256+
),
257+
(
258+
"@Directive({
259+
standalone: false,
260+
selector: 'x-selector'
261+
})
262+
class Test {}",
263+
"@Directive({
264+
,
265+
selector: 'x-selector'
266+
})
267+
class Test {}",
268+
),
269+
(
270+
"@Pipe({ standalone: false })
271+
class Test {}",
272+
"@Pipe({ })
273+
class Test {}",
274+
),
275+
(
276+
"@Pipe({
277+
standalone: false,
278+
name: 'pipe-name'
279+
})
280+
class Test {}",
281+
"@Pipe({
282+
,
283+
name: 'pipe-name'
284+
})
285+
class Test {}",
286+
),
287+
];
288+
Tester::new(PreferStandalone::NAME, PreferStandalone::PLUGIN, pass, fail)
289+
.expect_fix(fix)
290+
.test_and_snapshot();
291+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
source: crates/oxc_linter/src/tester.rs
3+
---
4+
eslint-plugin-angular(prefer-standalone): Prefer standalone components.
5+
╭─[prefer_standalone.tsx:1:14]
6+
1 │ @Component({ standalone: false })
7+
· ─────────────────
8+
2class Test {}
9+
╰────
10+
help: Replace `standalone: false` with ``.
11+
12+
eslint-plugin-angular(prefer-standalone): Prefer standalone components.
13+
╭─[prefer_standalone.tsx:2:13]
14+
1 │ @Component({
15+
2 │ standalone: false,
16+
· ─────────────────
17+
3 │ template: '<div></div>'
18+
╰────
19+
help: Replace `standalone: false` with ``.
20+
21+
eslint-plugin-angular(prefer-standalone): Prefer standalone components.
22+
╭─[prefer_standalone.tsx:1:14]
23+
1 │ @Directive({ standalone: false })
24+
· ─────────────────
25+
2class Test {}
26+
╰────
27+
help: Replace `standalone: false` with ``.
28+
29+
eslint-plugin-angular(prefer-standalone): Prefer standalone components.
30+
╭─[prefer_standalone.tsx:2:13]
31+
1 │ @Directive({
32+
2 │ standalone: false,
33+
· ─────────────────
34+
3 │ selector: 'x-selector'
35+
╰────
36+
help: Replace `standalone: false` with ``.
37+
38+
eslint-plugin-angular(prefer-standalone): Prefer standalone components.
39+
╭─[prefer_standalone.tsx:1:9]
40+
1 │ @Pipe({ standalone: false })
41+
· ─────────────────
42+
2class Test {}
43+
╰────
44+
help: Replace `standalone: false` with ``.
45+
46+
eslint-plugin-angular(prefer-standalone): Prefer standalone components.
47+
╭─[prefer_standalone.tsx:2:13]
48+
1 │ @Pipe({
49+
2 │ standalone: false,
50+
· ─────────────────
51+
3 │ name: 'pipe-name'
52+
╰────
53+
help: Replace `standalone: false` with ``.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
source: crates/oxc_linter/src/tester.rs
3+
---
4+
eslint(prefer-standalone): Prefer standalone components.
5+
╭─[prefer_standalone.tsx:1:26]
6+
1 │ @Component({ standalone: false })
7+
· ─────
8+
2class Test {}
9+
╰────

0 commit comments

Comments
 (0)