1use crate::control_flow::ControlFlow;
4use crate::diagnostic::{
5 LintDiagnostic, LintDiagnosticDetails, LintDiagnosticRange, LintDocsUrl,
6 LintFix,
7};
8use crate::ignore_directives::{
9 parse_line_ignore_directives, CodeStatus, FileIgnoreDirective,
10 LineIgnoreDirective,
11};
12use crate::linter::LinterContext;
13use crate::rules;
14use deno_ast::swc::ast::Expr;
15use deno_ast::swc::common::comments::Comment;
16use deno_ast::swc::common::util::take::Take;
17use deno_ast::swc::common::{SourceMap, SyntaxContext};
18use deno_ast::SourceTextInfo;
19use deno_ast::{
20 view as ast_view, ParsedSource, RootNode, SourcePos, SourceRange,
21};
22use deno_ast::{MediaType, ModuleSpecifier};
23use deno_ast::{MultiThreadedComments, Scope};
24use std::borrow::Cow;
25use std::collections::{HashMap, HashSet};
26use std::rc::Rc;
27
28pub struct Context<'a> {
30 parsed_source: ParsedSource,
31 diagnostics: Vec<LintDiagnostic>,
32 program: ast_view::Program<'a>,
33 file_ignore_directive: Option<FileIgnoreDirective>,
34 line_ignore_directives: HashMap<usize, LineIgnoreDirective>,
35 scope: Scope,
36 control_flow: ControlFlow,
37 traverse_flow: TraverseFlow,
38 check_unknown_rules: bool,
39 #[allow(clippy::redundant_allocation)] jsx_factory: Option<Rc<Box<Expr>>>,
41 #[allow(clippy::redundant_allocation)] jsx_fragment_factory: Option<Rc<Box<Expr>>>,
43}
44
45impl<'a> Context<'a> {
46 pub(crate) fn new(
47 linter_ctx: &'a LinterContext,
48 parsed_source: ParsedSource,
49 program: ast_view::Program<'a>,
50 file_ignore_directive: Option<FileIgnoreDirective>,
51 default_jsx_factory: Option<String>,
52 default_jsx_fragment_factory: Option<String>,
53 ) -> Self {
54 let line_ignore_directives = parse_line_ignore_directives(
55 linter_ctx.ignore_diagnostic_directive,
56 program,
57 );
58 let scope = Scope::analyze(program);
59 let control_flow =
60 ControlFlow::analyze(program, parsed_source.unresolved_context());
61
62 let mut jsx_factory = None;
63 let mut jsx_fragment_factory = None;
64
65 parsed_source.globals().with(|marks| {
66 let top_level_mark = marks.top_level;
67
68 if let Some(leading_comments) = parsed_source.get_leading_comments() {
69 let jsx_directives =
70 deno_ast::swc::transforms::react::JsxDirectives::from_comments(
71 &SourceMap::default(),
72 #[allow(clippy::disallowed_types)]
73 deno_ast::swc::common::Span::dummy(),
74 leading_comments,
75 top_level_mark,
76 );
77
78 jsx_factory = jsx_directives.pragma;
79 jsx_fragment_factory = jsx_directives.pragma_frag;
80 }
81
82 if jsx_factory.is_none() {
83 if let Some(factory) = default_jsx_factory {
84 jsx_factory = Some(Rc::new(
85 deno_ast::swc::transforms::react::parse_expr_for_jsx(
86 &SourceMap::default(),
87 "jsx",
88 factory.into(),
89 top_level_mark,
90 ),
91 ));
92 }
93 }
94 if jsx_fragment_factory.is_none() {
95 if let Some(factory) = default_jsx_fragment_factory {
96 jsx_fragment_factory = Some(Rc::new(
97 deno_ast::swc::transforms::react::parse_expr_for_jsx(
98 &SourceMap::default(),
99 "jsxFragment",
100 factory.into(),
101 top_level_mark,
102 ),
103 ));
104 }
105 }
106 });
107
108 Self {
109 file_ignore_directive,
110 line_ignore_directives,
111 scope,
112 control_flow,
113 program,
114 parsed_source,
115 diagnostics: Vec::new(),
116 traverse_flow: TraverseFlow::default(),
117 check_unknown_rules: linter_ctx.check_unknown_rules,
118 jsx_factory,
119 jsx_fragment_factory,
120 }
121 }
122
123 pub fn specifier(&self) -> &ModuleSpecifier {
125 self.parsed_source.specifier()
126 }
127
128 pub fn media_type(&self) -> MediaType {
131 self.parsed_source.media_type()
132 }
133
134 pub fn comments(&self) -> &MultiThreadedComments {
136 self.parsed_source.comments()
137 }
138
139 pub fn diagnostics(&self) -> &[LintDiagnostic] {
141 &self.diagnostics
142 }
143
144 pub fn parsed_source(&self) -> &ParsedSource {
146 &self.parsed_source
147 }
148
149 pub fn text_info(&self) -> &SourceTextInfo {
151 self.parsed_source.text_info_lazy()
152 }
153
154 pub fn program(&self) -> ast_view::Program<'a> {
157 self.program
158 }
159
160 pub fn file_ignore_directive(&self) -> Option<&FileIgnoreDirective> {
162 self.file_ignore_directive.as_ref()
163 }
164
165 pub fn line_ignore_directives(&self) -> &HashMap<usize, LineIgnoreDirective> {
168 &self.line_ignore_directives
169 }
170
171 pub fn scope(&self) -> &Scope {
173 &self.scope
174 }
175
176 pub fn control_flow(&self) -> &ControlFlow {
178 &self.control_flow
179 }
180
181 pub fn jsx_factory(&self) -> Option<Rc<Box<Expr>>> {
186 self.jsx_factory.clone()
187 }
188
189 pub fn jsx_fragment_factory(&self) -> Option<Rc<Box<Expr>>> {
194 self.jsx_fragment_factory.clone()
195 }
196
197 pub(crate) fn unresolved_ctxt(&self) -> SyntaxContext {
199 self.parsed_source.unresolved_context()
200 }
201
202 pub(crate) fn assert_traverse_init(&self) {
203 self.traverse_flow.assert_init();
204 }
205
206 pub(crate) fn should_stop_traverse(&mut self) -> bool {
207 self.traverse_flow.should_stop()
208 }
209
210 pub(crate) fn stop_traverse(&mut self) {
211 self.traverse_flow.set_stop_traverse();
212 }
213
214 pub fn all_comments(&self) -> impl Iterator<Item = &'a Comment> {
215 self.program.comment_container().all_comments()
216 }
217
218 pub fn leading_comments_at(
219 &self,
220 start: SourcePos,
221 ) -> impl Iterator<Item = &'a Comment> {
222 self.program.comment_container().leading_comments(start)
223 }
224
225 pub fn trailing_comments_at(
226 &self,
227 end: SourcePos,
228 ) -> impl Iterator<Item = &'a Comment> {
229 self.program.comment_container().trailing_comments(end)
230 }
231
232 pub(crate) fn check_ignore_directive_usage(&mut self) -> Vec<LintDiagnostic> {
237 let mut filtered = Vec::new();
238
239 for diagnostic in self.diagnostics.iter().cloned() {
240 if let Some(f) = self.file_ignore_directive.as_mut() {
241 if f.check_used(&diagnostic.details.code) {
242 continue;
243 }
244 }
245 let Some(range) = diagnostic.range.as_ref() else {
246 continue;
247 };
248
249 let diagnostic_line = range.text_info.line_index(range.range.start);
250 if diagnostic_line > 0 {
251 if let Some(l) =
252 self.line_ignore_directives.get_mut(&(diagnostic_line - 1))
253 {
254 if l.check_used(&diagnostic.details.code) {
255 continue;
256 }
257 }
258 }
259
260 filtered.push(diagnostic);
261 }
262
263 filtered
264 }
265
266 pub(crate) fn ban_unused_ignore(
270 &self,
271 known_rules_codes: &HashSet<Cow<'static, str>>,
272 ) -> Vec<LintDiagnostic> {
273 const CODE: &str = "ban-unused-ignore";
274
275 if self
278 .file_ignore_directive
279 .as_ref()
280 .is_some_and(|file_ignore| file_ignore.has_code(CODE))
281 {
282 return vec![];
283 }
284
285 let is_unused_code = |&(code, status): &(&String, &CodeStatus)| {
286 let is_unknown = !known_rules_codes.contains(code.as_str());
287 !status.used && !is_unknown
288 };
289
290 let mut diagnostics = Vec::new();
291
292 if let Some(file_ignore) = self.file_ignore_directive.as_ref() {
293 for (unused_code, _status) in
294 file_ignore.codes().iter().filter(is_unused_code)
295 {
296 let d = self.create_diagnostic(
297 Some(self.create_diagnostic_range(file_ignore.range())),
298 self.create_diagnostic_details(
299 CODE,
300 format!("Ignore for code \"{}\" was not used.", unused_code),
301 None,
302 Vec::new(),
303 ),
304 );
305 diagnostics.push(d);
306 }
307 }
308
309 for line_ignore in self.line_ignore_directives.values() {
310 for (unused_code, _status) in
315 line_ignore.codes().iter().filter(is_unused_code)
316 {
317 let d = self.create_diagnostic(
318 Some(self.create_diagnostic_range(line_ignore.range())),
319 self.create_diagnostic_details(
320 CODE,
321 format!("Ignore for code \"{}\" was not used.", unused_code),
322 None,
323 Vec::new(),
324 ),
325 );
326 diagnostics.push(d);
327 }
328 }
329
330 diagnostics
331 }
332
333 pub(crate) fn ban_unknown_rule_code(
338 &mut self,
339 enabled_rules: &HashSet<Cow<'static, str>>,
340 ) -> Vec<LintDiagnostic> {
341 let mut diagnostics = Vec::new();
342
343 if let Some(file_ignore) = self.file_ignore_directive.as_ref() {
344 for unknown_rule_code in file_ignore
345 .codes()
346 .keys()
347 .filter(|code| !enabled_rules.contains(code.as_str()))
348 {
349 let d = self.create_diagnostic(
350 Some(self.create_diagnostic_range(file_ignore.range())),
351 self.create_diagnostic_details(
352 rules::ban_unknown_rule_code::CODE,
353 format!("Unknown rule for code \"{}\"", unknown_rule_code),
354 None,
355 Vec::new(),
356 ),
357 );
358 diagnostics.push(d);
359 }
360 }
361
362 for line_ignore in self.line_ignore_directives.values() {
363 for unknown_rule_code in line_ignore
364 .codes()
365 .keys()
366 .filter(|code| !enabled_rules.contains(code.as_str()))
367 {
368 let d = self.create_diagnostic(
369 Some(self.create_diagnostic_range(line_ignore.range())),
370 self.create_diagnostic_details(
371 rules::ban_unknown_rule_code::CODE,
372 format!("Unknown rule for code \"{}\"", unknown_rule_code),
373 None,
374 Vec::new(),
375 ),
376 );
377 diagnostics.push(d);
378 }
379 }
380
381 if !diagnostics.is_empty() {
382 if let Some(f) = self.file_ignore_directive.as_mut() {
383 f.check_used(rules::ban_unknown_rule_code::CODE);
384 }
385 }
386
387 if self.check_unknown_rules
388 && !self
389 .file_ignore_directive()
390 .map(|f| f.has_code(rules::ban_unknown_rule_code::CODE))
391 .unwrap_or(false)
392 {
393 diagnostics
394 } else {
395 vec![]
396 }
397 }
398
399 pub fn add_diagnostic(
400 &mut self,
401 range: SourceRange,
402 code: impl ToString,
403 message: impl ToString,
404 ) {
405 self.add_diagnostic_details(
406 Some(self.create_diagnostic_range(range)),
407 self.create_diagnostic_details(
408 code,
409 message.to_string(),
410 None,
411 Vec::new(),
412 ),
413 );
414 }
415
416 pub fn add_diagnostic_with_hint(
417 &mut self,
418 range: SourceRange,
419 code: impl ToString,
420 message: impl ToString,
421 hint: impl ToString,
422 ) {
423 self.add_diagnostic_details(
424 Some(self.create_diagnostic_range(range)),
425 self.create_diagnostic_details(
426 code,
427 message,
428 Some(hint.to_string()),
429 Vec::new(),
430 ),
431 );
432 }
433
434 pub fn add_diagnostic_with_fixes(
435 &mut self,
436 range: SourceRange,
437 code: impl ToString,
438 message: impl ToString,
439 hint: Option<String>,
440 fixes: Vec<LintFix>,
441 ) {
442 self.add_diagnostic_details(
443 Some(self.create_diagnostic_range(range)),
444 self.create_diagnostic_details(code, message, hint, fixes),
445 );
446 }
447
448 pub fn add_diagnostic_details(
449 &mut self,
450 maybe_range: Option<LintDiagnosticRange>,
451 details: LintDiagnosticDetails,
452 ) {
453 self
454 .diagnostics
455 .push(self.create_diagnostic(maybe_range, details));
456 }
457
458 pub fn add_external_diagnostics(&mut self, diagnostics: &[LintDiagnostic]) {
463 self.diagnostics.extend_from_slice(diagnostics);
464 }
465
466 pub(crate) fn create_diagnostic(
467 &self,
468 maybe_range: Option<LintDiagnosticRange>,
469 details: LintDiagnosticDetails,
470 ) -> LintDiagnostic {
471 LintDiagnostic {
472 specifier: self.specifier().clone(),
473 range: maybe_range,
474 details,
475 }
476 }
477
478 pub(crate) fn create_diagnostic_details(
479 &self,
480 code: impl ToString,
481 message: impl ToString,
482 maybe_hint: Option<String>,
483 fixes: Vec<LintFix>,
484 ) -> LintDiagnosticDetails {
485 LintDiagnosticDetails {
486 message: message.to_string(),
487 code: code.to_string(),
488 hint: maybe_hint,
489 fixes,
490 custom_docs_url: LintDocsUrl::Default,
491 info: vec![],
492 }
493 }
494
495 pub(crate) fn create_diagnostic_range(
496 &self,
497 range: SourceRange,
498 ) -> LintDiagnosticRange {
499 LintDiagnosticRange {
500 range,
501 text_info: self.text_info().clone(),
502 description: None,
503 }
504 }
505}
506
507#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
513struct TraverseFlow {
514 stop_traverse: bool,
515}
516
517impl TraverseFlow {
518 fn set_stop_traverse(&mut self) {
519 self.stop_traverse = true;
520 }
521
522 fn reset(&mut self) {
523 self.stop_traverse = false;
524 }
525
526 fn assert_init(&self) {
527 assert!(!self.stop_traverse);
528 }
529
530 fn should_stop(&mut self) -> bool {
531 let stop = self.stop_traverse;
532 self.reset();
533 stop
534 }
535}