|
1 | 1 | use std::ops::Deref;
|
2 | 2 |
|
3 |
| -use itertools::Itertools; |
4 |
| -use oxc_ast::AstKind; |
| 3 | +use oxc_ast::{AstKind, ast::Statement}; |
5 | 4 | use oxc_diagnostics::OxcDiagnostic;
|
6 | 5 | use oxc_macros::declare_oxc_lint;
|
| 6 | +use oxc_semantic::AstNode; |
7 | 7 | use oxc_span::{CompactStr, GetSpan, Span};
|
8 | 8 |
|
9 | 9 | use crate::{
|
10 |
| - AstNode, |
11 | 10 | context::LintContext,
|
12 | 11 | rule::Rule,
|
13 | 12 | utils::{
|
14 |
| - JestGeneralFnKind, ParsedGeneralJestFnCall, collect_possible_jest_call_node, |
15 |
| - parse_general_jest_fn_call, |
| 13 | + JestGeneralFnKind, ParsedGeneralJestFnCall, PossibleJestNode, parse_general_jest_fn_call, |
16 | 14 | },
|
17 | 15 | };
|
18 | 16 |
|
@@ -67,94 +65,88 @@ declare_oxc_lint!(
|
67 | 65 | fix
|
68 | 66 | );
|
69 | 67 |
|
70 |
| -enum NodeType { |
71 |
| - Test(CompactStr), |
72 |
| - Statement, |
73 |
| -} |
74 |
| - |
75 | 68 | impl Rule for PaddingAroundTestBlocks {
|
76 |
| - fn run_once(&self, ctx: &LintContext<'_>) { |
77 |
| - let mut nodes = collect_possible_jest_call_node(ctx) |
78 |
| - .iter() |
79 |
| - .filter_map(|possible_jest_node| { |
80 |
| - let node = possible_jest_node.node; |
81 |
| - let AstKind::CallExpression(call_expr) = node.kind() else { |
82 |
| - return None; |
| 69 | + fn run_on_jest_node<'a, 'c>( |
| 70 | + &self, |
| 71 | + jest_node: &PossibleJestNode<'a, 'c>, |
| 72 | + ctx: &'c LintContext<'a>, |
| 73 | + ) { |
| 74 | + let node = jest_node.node; |
| 75 | + let AstKind::CallExpression(call_expr) = node.kind() else { |
| 76 | + return; |
| 77 | + }; |
| 78 | + let Some(jest_fn_call) = parse_general_jest_fn_call(call_expr, jest_node, ctx) else { |
| 79 | + return; |
| 80 | + }; |
| 81 | + let ParsedGeneralJestFnCall { kind, name, .. } = &jest_fn_call; |
| 82 | + let Some(kind) = kind.to_general() else { |
| 83 | + return; |
| 84 | + }; |
| 85 | + if kind != JestGeneralFnKind::Test { |
| 86 | + return; |
| 87 | + } |
| 88 | + let scope_node = ctx.nodes().get_node(ctx.scoping().get_node_id(node.scope_id())); |
| 89 | + let prev_statement_span = match scope_node.kind() { |
| 90 | + AstKind::Program(program) => { |
| 91 | + get_statement_span_before_node(*node, program.body.as_slice()) |
| 92 | + } |
| 93 | + AstKind::ArrowFunctionExpression(arrow_func_expr) => { |
| 94 | + get_statement_span_before_node(*node, arrow_func_expr.body.statements.as_slice()) |
| 95 | + } |
| 96 | + AstKind::Function(function) => { |
| 97 | + let Some(body) = &function.body else { |
| 98 | + return; |
83 | 99 | };
|
84 |
| - let jest_fn_call = parse_general_jest_fn_call(call_expr, possible_jest_node, ctx)?; |
85 |
| - let ParsedGeneralJestFnCall { kind, name, .. } = &jest_fn_call; |
86 |
| - let kind = kind.to_general()?; |
87 |
| - if kind != JestGeneralFnKind::Test { |
88 |
| - return None; |
89 |
| - } |
90 |
| - Some((NodeType::Test(name.deref().into()), node)) |
91 |
| - }) |
92 |
| - .collect_vec(); |
93 |
| - for node in ctx.nodes() { |
94 |
| - if node.kind().is_statement() { |
95 |
| - nodes.push((NodeType::Statement, node)); |
| 100 | + get_statement_span_before_node(*node, body.statements.as_slice()) |
96 | 101 | }
|
97 |
| - } |
98 |
| - nodes.sort_by_key(|(_, node)| node.span().end); |
99 |
| - let mut prev_node: Option<&AstNode<'_>> = None; |
100 |
| - for (node_type, node) in nodes { |
101 |
| - match node_type { |
102 |
| - NodeType::Test(name) => { |
103 |
| - if let Some(prev_node) = prev_node { |
104 |
| - if prev_node.span().end > node.span().start { |
105 |
| - continue; |
106 |
| - } |
107 |
| - let mut comments_range = |
108 |
| - ctx.comments_range(prev_node.span().end..node.span().start); |
109 |
| - let mut span_between_start = prev_node.span().end; |
110 |
| - let mut span_between_end = node.span().start; |
111 |
| - if let Some(last_comment_span) = |
112 |
| - comments_range.next_back().map(|comment| comment.span) |
113 |
| - { |
114 |
| - let space_after_last_comment = ctx |
115 |
| - .source_range(Span::new(last_comment_span.end, node.span().start)); |
116 |
| - let space_before_last_comment = ctx.source_range(Span::new( |
117 |
| - prev_node.span().end, |
118 |
| - last_comment_span.start, |
119 |
| - )); |
120 |
| - if space_after_last_comment.matches('\n').count() > 1 |
121 |
| - || space_before_last_comment.matches('\n').count() == 0 |
122 |
| - { |
123 |
| - span_between_start = last_comment_span.end; |
124 |
| - } else { |
125 |
| - span_between_end = last_comment_span.start; |
126 |
| - } |
127 |
| - } |
128 |
| - let span_between = Span::new(span_between_start, span_between_end); |
129 |
| - let content = ctx.source_range(span_between); |
130 |
| - if content.matches('\n').count() < 2 { |
131 |
| - ctx.diagnostic_with_fix( |
132 |
| - padding_around_test_blocks_diagnostic( |
133 |
| - Span::new(span_between_end, span_between_end), |
134 |
| - &name, |
135 |
| - ), |
136 |
| - |fixer| { |
137 |
| - let spaces_after_last_line = content |
138 |
| - .rfind('\n') |
139 |
| - .map_or("", |index| content.split_at(index + 1).1); |
140 |
| - fixer.replace( |
141 |
| - span_between, |
142 |
| - format!("\n\n{spaces_after_last_line}"), |
143 |
| - ) |
144 |
| - }, |
145 |
| - ); |
146 |
| - } |
147 |
| - } |
148 |
| - prev_node = Some(node); |
149 |
| - } |
150 |
| - NodeType::Statement => { |
151 |
| - prev_node = Some(node); |
152 |
| - } |
| 102 | + _ => None, |
| 103 | + }; |
| 104 | + let Some(prev_statement_span) = prev_statement_span else { |
| 105 | + return; |
| 106 | + }; |
| 107 | + let mut comments_range = ctx.comments_range(prev_statement_span.end..node.span().start); |
| 108 | + let mut span_between_start = prev_statement_span.end; |
| 109 | + let mut span_between_end = node.span().start; |
| 110 | + if let Some(last_comment_span) = comments_range.next_back().map(|comment| comment.span) { |
| 111 | + let space_after_last_comment = |
| 112 | + ctx.source_range(Span::new(last_comment_span.end, node.span().start)); |
| 113 | + let space_before_last_comment = |
| 114 | + ctx.source_range(Span::new(prev_statement_span.end, last_comment_span.start)); |
| 115 | + if space_after_last_comment.matches('\n').count() > 1 |
| 116 | + || space_before_last_comment.matches('\n').count() == 0 |
| 117 | + { |
| 118 | + span_between_start = last_comment_span.end; |
| 119 | + } else { |
| 120 | + span_between_end = last_comment_span.start; |
153 | 121 | }
|
154 | 122 | }
|
| 123 | + let span_between = Span::new(span_between_start, span_between_end); |
| 124 | + let content = ctx.source_range(span_between); |
| 125 | + if content.matches('\n').count() < 2 { |
| 126 | + ctx.diagnostic_with_fix( |
| 127 | + padding_around_test_blocks_diagnostic( |
| 128 | + Span::new(span_between_end, span_between_end), |
| 129 | + &name.deref().into(), |
| 130 | + ), |
| 131 | + |fixer| { |
| 132 | + let whitespace_after_last_line = |
| 133 | + content.rfind('\n').map_or("", |index| content.split_at(index + 1).1); |
| 134 | + fixer.replace(span_between, format!("\n\n{whitespace_after_last_line}")) |
| 135 | + }, |
| 136 | + ); |
| 137 | + } |
155 | 138 | }
|
156 | 139 | }
|
157 | 140 |
|
| 141 | +fn get_statement_span_before_node(node: AstNode, statements: &[Statement]) -> Option<Span> { |
| 142 | + statements |
| 143 | + .iter() |
| 144 | + .filter_map(|statement| { |
| 145 | + if statement.span().end <= node.span().start { Some(statement.span()) } else { None } |
| 146 | + }) |
| 147 | + .next_back() |
| 148 | +} |
| 149 | + |
158 | 150 | #[test]
|
159 | 151 | fn test() {
|
160 | 152 | use crate::tester::Tester;
|
@@ -198,6 +190,14 @@ test('bar bar', () => {});
|
198 | 190 | .skip('skippy skip', () => {});
|
199 | 191 | xit('bar foo', () => {});
|
200 | 192 | ",
|
| 193 | + r" |
| 194 | +describe('other bar', function() { |
| 195 | + test('is another bar w/ test', () => { |
| 196 | + }); |
| 197 | + it('is another bar w/ it', () => { |
| 198 | + }); |
| 199 | +}); |
| 200 | + ", |
201 | 201 | ];
|
202 | 202 |
|
203 | 203 | let fix = vec![
|
@@ -281,8 +281,28 @@ test
|
281 | 281 | xit('bar foo', () => {});
|
282 | 282 | ",
|
283 | 283 | ),
|
| 284 | + ( |
| 285 | + r" |
| 286 | +describe('other bar', function() { |
| 287 | + test('is another bar w/ test', () => { |
| 288 | + }); |
| 289 | + it('is another bar w/ it', () => { |
| 290 | + }); |
| 291 | +}); |
| 292 | + ", |
| 293 | + r" |
| 294 | +describe('other bar', function() { |
| 295 | + test('is another bar w/ test', () => { |
| 296 | + }); |
| 297 | +
|
| 298 | + it('is another bar w/ it', () => { |
| 299 | + }); |
| 300 | +}); |
| 301 | + ", |
| 302 | + ), |
284 | 303 | ];
|
285 | 304 | Tester::new(PaddingAroundTestBlocks::NAME, PaddingAroundTestBlocks::PLUGIN, pass, fail)
|
| 305 | + .with_jest_plugin(true) |
286 | 306 | .expect_fix(fix)
|
287 | 307 | .test_and_snapshot();
|
288 | 308 | }
|
0 commit comments