Skip to content

Commit ff775e9

Browse files
DonIsaacgraphite-app[bot]camc314
authored
fix(linter/consistent-function-scoping): descriptive diagnostic labels (#11682)
## What This PR Does Makes diagnostic labels produced by `unicorn/consistent-function-scoping` much more descriptive. For example: ``` ⚠ eslint-plugin-unicorn(consistent-function-scoping): Function does not capture any variables from the outer scope. ╭─[consistent_function_scoping.tsx:3:30] 1 │ function doFoo(foo) { · ──┬── · ╰── Function `doBar` is declared in this scope 2 │ { 3 │ function doBar(bar) { · ──┬── · ╰── It does not capture any variables declared there 4 │ return bar; ╰──── help: Move this function to the outer scope. ``` --------- Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> Co-authored-by: Cameron Clark <cameron.clark@hey.com>
1 parent bf8263d commit ff775e9

File tree

2 files changed

+394
-164
lines changed

2 files changed

+394
-164
lines changed

crates/oxc_linter/src/rules/unicorn/consistent_function_scoping.rs

Lines changed: 120 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
use rustc_hash::FxHashSet;
22

3-
use oxc_ast::{
4-
AstKind,
5-
ast::{Function, IdentifierReference, JSXElementName, ThisExpression},
6-
};
3+
use oxc_ast::{AstKind, ast::*};
74
use oxc_ast_visit::{Visit, walk};
85
use oxc_diagnostics::OxcDiagnostic;
96
use oxc_macros::declare_oxc_lint;
10-
use oxc_semantic::{ReferenceId, ScopeFlags};
11-
use oxc_span::{GetSpan, Span};
7+
use oxc_semantic::{ReferenceId, ScopeFlags, ScopeId, SymbolId};
8+
use oxc_span::{Atom, GetSpan, Span};
129

1310
use crate::{
1411
AstNode,
@@ -18,10 +15,41 @@ use crate::{
1815
utils::is_react_hook,
1916
};
2017

21-
fn consistent_function_scoping(span: Span) -> OxcDiagnostic {
22-
OxcDiagnostic::warn("Function does not capture any variables from the outer scope.")
23-
.with_help("Move this function to the outer scope.")
24-
.with_label(span)
18+
fn consistent_function_scoping(
19+
fn_span: Span,
20+
parent_scope_span: Option<Span>,
21+
parent_scope_kind: Option<&'static str>,
22+
function_name: Option<Atom<'_>>,
23+
) -> OxcDiagnostic {
24+
debug_assert!(Some(fn_span) != parent_scope_span);
25+
let function_label = if let Some(name) = function_name {
26+
format!("Function `{name}` does not capture any variables from its parent scope")
27+
} else {
28+
"This function does not use any variables from its parent scope".into()
29+
};
30+
31+
let d = OxcDiagnostic::warn(function_label).with_help(match function_name {
32+
Some(name) => {
33+
format!("Move `{name}` to the outer scope to avoid recreating it on every call.")
34+
}
35+
None => {
36+
"Move this function to the outer scope to avoid recreating it on every call.".into()
37+
}
38+
});
39+
40+
match parent_scope_span {
41+
Some(parent) => d.with_labels([
42+
parent.label("Outer scope where this function is defined"),
43+
fn_span.primary_label(if let Some(parent_scope_kind) = parent_scope_kind {
44+
format!(
45+
"This function does not use any variables from the parent {parent_scope_kind}"
46+
)
47+
} else {
48+
"This function does not use any variables from here".into()
49+
}),
50+
]),
51+
None => d.with_label(fn_span),
52+
}
2553
}
2654

2755
#[derive(Debug, Default, Clone)]
@@ -148,7 +176,7 @@ impl Rule for ConsistentFunctionScoping {
148176
}
149177

150178
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
151-
let (function_declaration_symbol_id, function_body, reporter_span) =
179+
let (function_declaration_symbol_id, function_name, function_body, reporter_span) =
152180
match node.kind() {
153181
AstKind::Function(function) => {
154182
if function.is_typescript_syntax() {
@@ -176,14 +204,20 @@ impl Rule for ConsistentFunctionScoping {
176204
if let Some(binding_ident) = get_function_like_declaration(node, ctx) {
177205
(
178206
binding_ident.symbol_id(),
207+
Some(binding_ident.name),
179208
function_body,
180209
function.id.as_ref().map_or(
181210
Span::sized(function.span.start, 8),
182211
|func_binding_ident| func_binding_ident.span,
183212
),
184213
)
185214
} else if let Some(function_id) = &function.id {
186-
(function_id.symbol_id(), function_body, function_id.span())
215+
(
216+
function_id.symbol_id(),
217+
Some(function_id.name),
218+
function_body,
219+
function_id.span(),
220+
)
187221
} else {
188222
return;
189223
}
@@ -193,7 +227,12 @@ impl Rule for ConsistentFunctionScoping {
193227
return;
194228
};
195229

196-
(binding_ident.symbol_id(), &arrow_function.body, binding_ident.span())
230+
(
231+
binding_ident.symbol_id(),
232+
Some(binding_ident.name),
233+
&arrow_function.body,
234+
binding_ident.span(),
235+
)
197236
}
198237
_ => return,
199238
};
@@ -249,7 +288,16 @@ impl Rule for ConsistentFunctionScoping {
249288
}
250289
}
251290

252-
ctx.diagnostic(consistent_function_scoping(reporter_span));
291+
let (maybe_parent_scope_span, maybe_parent_scope_type) =
292+
get_short_span_for_fn_scope(ctx, function_declaration_symbol_id, scope)
293+
.map_or((None, None), |(span, rtype)| (Some(span), Some(rtype)));
294+
295+
ctx.diagnostic(consistent_function_scoping(
296+
reporter_span,
297+
maybe_parent_scope_span,
298+
maybe_parent_scope_type,
299+
function_name,
300+
));
253301
}
254302
}
255303

@@ -311,6 +359,64 @@ fn is_in_react_hook<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>) -> bool {
311359
false
312360
}
313361

362+
fn get_short_span_for_fn_scope(
363+
ctx: &LintContext<'_>,
364+
function_symbol_id: SymbolId,
365+
scope_id: ScopeId,
366+
) -> Option<(Span, &'static str)> {
367+
let scoping = ctx.scoping();
368+
369+
debug_assert!(!scoping.scope_flags(scope_id).contains(ScopeFlags::Top));
370+
371+
let scope_id =
372+
match ctx.nodes().parent_kind(ctx.scoping().symbol_declaration(function_symbol_id)) {
373+
Some(AstKind::AssignmentExpression(_)) => {
374+
ctx.scoping().scope_parent_id(scope_id).unwrap_or(scope_id)
375+
}
376+
_ => scope_id,
377+
};
378+
379+
let node_creating_parent_scope = ctx.nodes().get_node(scoping.get_node_id(scope_id));
380+
381+
match node_creating_parent_scope.kind() {
382+
AstKind::Function(f) => f.id.as_ref().map(|id| (id.span(), "function")),
383+
AstKind::ArrowFunctionExpression(_) => {
384+
let parent = ctx.nodes().parent_kind(node_creating_parent_scope.id())?;
385+
match parent {
386+
AstKind::VariableDeclarator(v) => Some((v.id.span(), "arrow function")),
387+
AstKind::AssignmentExpression(a) => Some((a.left.span(), "arrow function")),
388+
_ => None,
389+
}
390+
}
391+
AstKind::Class(c) => c.id.as_ref().map(|id| (id.span(), "class")),
392+
// only cover keywords of control flow statements
393+
AstKind::ForInStatement(ForInStatement { span, .. })
394+
| AstKind::ForOfStatement(ForOfStatement { span, .. })
395+
| AstKind::ForStatement(ForStatement { span, .. }) => {
396+
Some((Span::sized(span.start, 3), "for loop"))
397+
}
398+
AstKind::TryStatement(TryStatement { span, .. }) => {
399+
Some((Span::sized(span.start, 3), "try statement"))
400+
}
401+
AstKind::IfStatement(IfStatement { span, .. }) => {
402+
Some((Span::sized(span.start, 2), "if statement"))
403+
}
404+
AstKind::DoWhileStatement(DoWhileStatement { span, .. }) => {
405+
Some((Span::sized(span.start, 2), "do while statement"))
406+
}
407+
AstKind::SwitchStatement(SwitchStatement { span, .. }) => {
408+
Some((Span::sized(span.start, 6), "switch statement"))
409+
}
410+
AstKind::WhileStatement(WhileStatement { span, .. }) => {
411+
Some((Span::sized(span.start, 5), "while statement"))
412+
}
413+
AstKind::CatchClause(CatchClause { span, .. }) => {
414+
Some((Span::sized(span.start, 5), "catch block"))
415+
}
416+
_ => None,
417+
}
418+
}
419+
314420
#[test]
315421
fn test() {
316422
use crate::tester::Tester;

0 commit comments

Comments
 (0)