Skip to content

Commit 3016f03

Browse files
committed
feat(linter): let fixer functions return a None fix (#4210)
Part of #4187. Adds `CompositeFix::None`, which enables fixer functions to decide not to fix some code. While I was in the area, I took the liberty of adding some doc comments.
1 parent dd07a54 commit 3016f03

File tree

4 files changed

+104
-18
lines changed

4 files changed

+104
-18
lines changed

crates/oxc_linter/src/context.rs

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#![allow(rustdoc::private_intra_doc_links)] // useful for intellisense
12
use std::{cell::RefCell, path::Path, rc::Rc, sync::Arc};
23

34
use oxc_cfg::ControlFlowGraph;
@@ -18,11 +19,16 @@ use crate::{
1819
pub struct LintContext<'a> {
1920
semantic: Rc<Semantic<'a>>,
2021

22+
/// Diagnostics reported by the linter.
23+
///
24+
/// Contains diagnostics for all rules across all files.
2125
diagnostics: RefCell<Vec<Message<'a>>>,
2226

2327
disable_directives: Rc<DisableDirectives<'a>>,
2428

25-
/// Whether or not to apply code fixes during linting.
29+
/// Whether or not to apply code fixes during linting. Defaults to `false`.
30+
///
31+
/// Set via the `--fix` CLI flag.
2632
fix: bool,
2733

2834
file_path: Rc<Path>,
@@ -32,13 +38,24 @@ pub struct LintContext<'a> {
3238
// states
3339
current_rule_name: &'static str,
3440

41+
/// Current rule severity. Allows for user severity overrides, e.g.
42+
/// ```json
43+
/// // .oxlintrc.json
44+
/// {
45+
/// "rules": {
46+
/// "no-debugger": "error"
47+
/// }
48+
/// }
49+
/// ```
3550
severity: Severity,
3651
}
3752

3853
impl<'a> LintContext<'a> {
3954
/// # Panics
4055
/// If `semantic.cfg()` is `None`.
4156
pub fn new(file_path: Box<Path>, semantic: Rc<Semantic<'a>>) -> Self {
57+
const DIAGNOSTICS_INITIAL_CAPACITY: usize = 128;
58+
4259
// We should always check for `semantic.cfg()` being `Some` since we depend on it and it is
4360
// unwrapped without any runtime checks after construction.
4461
assert!(
@@ -50,7 +67,7 @@ impl<'a> LintContext<'a> {
5067
.build();
5168
Self {
5269
semantic,
53-
diagnostics: RefCell::new(vec![]),
70+
diagnostics: RefCell::new(Vec::with_capacity(DIAGNOSTICS_INITIAL_CAPACITY)),
5471
disable_directives: Rc::new(disable_directives),
5572
fix: false,
5673
file_path: file_path.into(),
@@ -60,6 +77,7 @@ impl<'a> LintContext<'a> {
6077
}
6178
}
6279

80+
/// Enable/disable automatic code fixes.
6381
#[must_use]
6482
pub fn with_fix(mut self, fix: bool) -> Self {
6583
self.fix = fix;
@@ -112,14 +130,17 @@ impl<'a> LintContext<'a> {
112130
span.source_text(self.semantic().source_text())
113131
}
114132

133+
/// [`SourceType`] of the file currently being linted.
115134
pub fn source_type(&self) -> &SourceType {
116135
self.semantic().source_type()
117136
}
118137

138+
/// Path to the file currently being linted.
119139
pub fn file_path(&self) -> &Path {
120140
&self.file_path
121141
}
122142

143+
/// Plugin settings
123144
pub fn settings(&self) -> &OxlintSettings {
124145
&self.eslint_config.settings
125146
}
@@ -128,6 +149,9 @@ impl<'a> LintContext<'a> {
128149
&self.eslint_config.globals
129150
}
130151

152+
/// Runtime environments turned on/off by the user.
153+
///
154+
/// Examples of environments are `builtin`, `browser`, `node`, etc.
131155
pub fn env(&self) -> &OxlintEnv {
132156
&self.eslint_config.env
133157
}
@@ -174,6 +198,11 @@ impl<'a> LintContext<'a> {
174198
}
175199

176200
/// Report a lint rule violation and provide an automatic fix.
201+
///
202+
/// The second argument is a [closure] that takes a [`RuleFixer`] and
203+
/// returns something that can turn into a [`CompositeFix`].
204+
///
205+
/// [closure]: <https://doc.rust-lang.org/book/ch13-01-closures.html>
177206
pub fn diagnostic_with_fix<C, F>(&self, diagnostic: OxcDiagnostic, fix: F)
178207
where
179208
C: Into<CompositeFix<'a>>,
@@ -189,23 +218,37 @@ impl<'a> LintContext<'a> {
189218
}
190219
}
191220

221+
/// AST nodes
222+
///
223+
/// Shorthand for `self.semantic().nodes()`.
192224
pub fn nodes(&self) -> &AstNodes<'a> {
193225
self.semantic().nodes()
194226
}
195227

228+
/// Scope tree
229+
///
230+
/// Shorthand for `ctx.semantic().scopes()`.
196231
pub fn scopes(&self) -> &ScopeTree {
197232
self.semantic().scopes()
198233
}
199234

235+
/// Symbol table
236+
///
237+
/// Shorthand for `ctx.semantic().symbols()`.
200238
pub fn symbols(&self) -> &SymbolTable {
201239
self.semantic().symbols()
202240
}
203241

242+
/// Imported modules and exported symbols
243+
///
244+
/// Shorthand for `ctx.semantic().module_record()`.
204245
pub fn module_record(&self) -> &ModuleRecord {
205246
self.semantic().module_record()
206247
}
207248

208-
/* JSDoc */
249+
/// JSDoc comments
250+
///
251+
/// Shorthand for `ctx.semantic().jsdoc()`.
209252
pub fn jsdoc(&self) -> &JSDocFinder<'a> {
210253
self.semantic().jsdoc()
211254
}

crates/oxc_linter/src/fixer.rs

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,22 @@ use std::borrow::Cow;
22

33
use oxc_codegen::Codegen;
44
use oxc_diagnostics::OxcDiagnostic;
5-
use oxc_span::{GetSpan, Span};
5+
use oxc_span::{GetSpan, Span, SPAN};
66

77
use crate::LintContext;
88

9-
#[derive(Debug, Clone, Default)]
9+
#[derive(Debug, Clone)]
1010
pub struct Fix<'a> {
1111
pub content: Cow<'a, str>,
1212
pub span: Span,
1313
}
1414

15+
impl Default for Fix<'_> {
16+
fn default() -> Self {
17+
Self::empty()
18+
}
19+
}
20+
1521
impl<'a> Fix<'a> {
1622
pub const fn delete(span: Span) -> Self {
1723
Self { content: Cow::Borrowed(""), span }
@@ -20,10 +26,21 @@ impl<'a> Fix<'a> {
2026
pub fn new<T: Into<Cow<'a, str>>>(content: T, span: Span) -> Self {
2127
Self { content: content.into(), span }
2228
}
29+
30+
/// Creates a [`Fix`] that doesn't change the source code.
31+
#[inline]
32+
pub const fn empty() -> Self {
33+
Self { content: Cow::Borrowed(""), span: SPAN }
34+
}
2335
}
2436

37+
#[derive(Debug, Default)]
2538
pub enum CompositeFix<'a> {
39+
/// No fixes
40+
#[default]
41+
None,
2642
Single(Fix<'a>),
43+
/// Several fixes that will be merged into one, in order.
2744
Multiple(Vec<Fix<'a>>),
2845
}
2946

@@ -33,9 +50,22 @@ impl<'a> From<Fix<'a>> for CompositeFix<'a> {
3350
}
3451
}
3552

53+
impl<'a> From<Option<Fix<'a>>> for CompositeFix<'a> {
54+
fn from(fix: Option<Fix<'a>>) -> Self {
55+
match fix {
56+
Some(fix) => CompositeFix::Single(fix),
57+
None => CompositeFix::None,
58+
}
59+
}
60+
}
61+
3662
impl<'a> From<Vec<Fix<'a>>> for CompositeFix<'a> {
3763
fn from(fixes: Vec<Fix<'a>>) -> Self {
38-
CompositeFix::Multiple(fixes)
64+
if fixes.is_empty() {
65+
CompositeFix::None
66+
} else {
67+
CompositeFix::Multiple(fixes)
68+
}
3969
}
4070
}
4171

@@ -46,19 +76,21 @@ impl<'a> CompositeFix<'a> {
4676
match self {
4777
CompositeFix::Single(fix) => fix,
4878
CompositeFix::Multiple(fixes) => Self::merge_fixes(fixes, source_text),
79+
CompositeFix::None => Fix::empty(),
4980
}
5081
}
51-
// Merges multiple fixes to one, returns an `Fix::default`(which will not fix anything) if:
52-
// 1. `fixes` is empty
53-
// 2. contains overlapped ranges
54-
// 3. contains negative ranges (span.start > span.end)
55-
// <https://github.com/eslint/eslint/blob/main/lib/linter/report-translator.js#L147-L179>
82+
/// Merges multiple fixes to one, returns an `Fix::default`(which will not fix anything) if:
83+
///
84+
/// 1. `fixes` is empty
85+
/// 2. contains overlapped ranges
86+
/// 3. contains negative ranges (span.start > span.end)
87+
///
88+
/// <https://github.com/eslint/eslint/blob/main/lib/linter/report-translator.js#L147-L179>
5689
fn merge_fixes(fixes: Vec<Fix<'a>>, source_text: &str) -> Fix<'a> {
5790
let mut fixes = fixes;
58-
let empty_fix = Fix::default();
5991
if fixes.is_empty() {
6092
// Do nothing
61-
return empty_fix;
93+
return Fix::empty();
6294
}
6395
if fixes.len() == 1 {
6496
return fixes.pop().unwrap();
@@ -77,33 +109,35 @@ impl<'a> CompositeFix<'a> {
77109
// negative range or overlapping ranges is invalid
78110
if span.start > span.end {
79111
debug_assert!(false, "Negative range is invalid: {span:?}");
80-
return empty_fix;
112+
return Fix::empty();
81113
}
82114
if last_pos > span.start {
83115
debug_assert!(
84116
false,
85117
"Fix must not be overlapped, last_pos: {}, span.start: {}",
86118
last_pos, span.start
87119
);
88-
return empty_fix;
120+
return Fix::empty();
89121
}
90122

91123
let Some(before) = source_text.get((last_pos) as usize..span.start as usize) else {
92124
debug_assert!(false, "Invalid range: {}, {}", last_pos, span.start);
93-
return empty_fix;
125+
return Fix::empty();
94126
};
95127

128+
output.reserve(before.len() + content.len());
96129
output.push_str(before);
97130
output.push_str(content);
98131
last_pos = span.end;
99132
}
100133

101134
let Some(after) = source_text.get(last_pos as usize..end as usize) else {
102135
debug_assert!(false, "Invalid range: {:?}", last_pos as usize..end as usize);
103-
return empty_fix;
136+
return Fix::empty();
104137
};
105138

106139
output.push_str(after);
140+
output.shrink_to_fit();
107141
Fix::new(output, Span::new(start, end))
108142
}
109143
}
@@ -121,6 +155,8 @@ impl<'c, 'a: 'c> RuleFixer<'c, 'a> {
121155
Self { ctx }
122156
}
123157

158+
/// Get a snippet of source text covered by the given [`Span`]. For details,
159+
/// see [`Span::source_text`].
124160
pub fn source_range(self, span: Span) -> &'a str {
125161
self.ctx.source_range(span)
126162
}
@@ -186,6 +222,11 @@ impl<'c, 'a: 'c> RuleFixer<'c, 'a> {
186222
pub fn codegen(self) -> Codegen<'a, false> {
187223
Codegen::<false>::new()
188224
}
225+
226+
#[allow(clippy::unused_self)]
227+
pub fn noop(self) -> Fix<'a> {
228+
Fix::empty()
229+
}
189230
}
190231

191232
pub struct FixResult<'a> {

crates/oxc_linter/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ fn size_asserts() {
4949
assert_eq_size!(RuleEnum, [u8; 16]);
5050
}
5151

52+
#[derive(Debug)]
5253
pub struct Linter {
5354
rules: Vec<RuleWithSeverity>,
5455
options: LintOptions,
@@ -83,6 +84,7 @@ impl Linter {
8384
self
8485
}
8586

87+
/// Enable/disable automatic code fixes.
8688
#[must_use]
8789
pub fn with_fix(mut self, yes: bool) -> Self {
8890
self.options.fix = yes;

crates/oxc_linter/src/rule.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ impl fmt::Display for RuleCategory {
100100
}
101101
}
102102

103-
#[derive(Clone)]
103+
#[derive(Debug, Clone)]
104104
pub struct RuleWithSeverity {
105105
pub rule: RuleEnum,
106106
pub severity: AllowWarnDeny,

0 commit comments

Comments
 (0)