deno_lint/
context.rs

1// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2
3use 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
28/// `Context` stores all data needed to perform linting of a particular file.
29pub 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)] // This type comes from SWC.
40  jsx_factory: Option<Rc<Box<Expr>>>,
41  #[allow(clippy::redundant_allocation)] // This type comes from SWC.
42  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  /// File specifier on which the lint rule is run.
124  pub fn specifier(&self) -> &ModuleSpecifier {
125    self.parsed_source.specifier()
126  }
127
128  /// The media type which linter was configured with. Can be used
129  /// to skip checking some rules.
130  pub fn media_type(&self) -> MediaType {
131    self.parsed_source.media_type()
132  }
133
134  /// Comment collection.
135  pub fn comments(&self) -> &MultiThreadedComments {
136    self.parsed_source.comments()
137  }
138
139  /// Stores diagnostics that are generated while linting
140  pub fn diagnostics(&self) -> &[LintDiagnostic] {
141    &self.diagnostics
142  }
143
144  /// Parsed source of the program.
145  pub fn parsed_source(&self) -> &ParsedSource {
146    &self.parsed_source
147  }
148
149  /// Information about the file text.
150  pub fn text_info(&self) -> &SourceTextInfo {
151    self.parsed_source.text_info_lazy()
152  }
153
154  /// The AST view of the program, which for example can be used for getting
155  /// comments
156  pub fn program(&self) -> ast_view::Program<'a> {
157    self.program
158  }
159
160  /// File-level ignore directive (`deno-lint-ignore-file`)
161  pub fn file_ignore_directive(&self) -> Option<&FileIgnoreDirective> {
162    self.file_ignore_directive.as_ref()
163  }
164
165  /// The map that stores line-level ignore directives (`deno-lint-ignore`).
166  /// The key of the map is line number.
167  pub fn line_ignore_directives(&self) -> &HashMap<usize, LineIgnoreDirective> {
168    &self.line_ignore_directives
169  }
170
171  /// Scope analysis result
172  pub fn scope(&self) -> &Scope {
173    &self.scope
174  }
175
176  /// Control-flow analysis result
177  pub fn control_flow(&self) -> &ControlFlow {
178    &self.control_flow
179  }
180
181  /// Get the JSX factory expression for this file, if one is specified (via
182  /// pragma or using a default). If this file is not JSX, uses the automatic
183  /// transform, or the default factory is not specified, this will return
184  /// `None`.
185  pub fn jsx_factory(&self) -> Option<Rc<Box<Expr>>> {
186    self.jsx_factory.clone()
187  }
188
189  /// Get the JSX fragment factory expression for this file, if one is specified
190  /// (via pragma or using a default). If this file is not JSX, uses the
191  /// automatic transform, or the default factory is not specified, this will
192  /// return `None`.
193  pub fn jsx_fragment_factory(&self) -> Option<Rc<Box<Expr>>> {
194    self.jsx_fragment_factory.clone()
195  }
196
197  /// The `SyntaxContext` of any unresolved identifiers
198  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  /// Mark ignore directives as used if that directive actually suppresses some
233  /// diagnostic, and return a list of diagnostics that are not ignored.
234  /// Make sure that this method is called after all lint rules have been
235  /// executed.
236  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  /// Lint rule implementation for `ban-unused-ignore`.
267  /// This should be run after all normal rules have been finished because this
268  /// works for diagnostics reported by other rules.
269  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 there's a file-level ignore directive containing `ban-unused-ignore`,
276    // exit without running this rule.
277    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      // We do nothing special even if the line-level ignore directive contains
311      // `ban-unused-ignore`. `ban-unused-ignore` can be ignored only via the
312      // file-level directive.
313
314      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  // TODO(bartlomieju): this should be a regular lint rule, not a mathod on this
334  // struct.
335  /// Lint rule implementation for `ban-unknown-rule-code`.
336  /// This should be run after all normal rules.
337  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  /// Add fully constructed diagnostics.
459  ///
460  /// This function can be used by the "external linter" to provide its own
461  /// diagnostics.
462  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/// A struct containing a boolean value to control whether a node's children
508/// will be traversed or not.
509/// If there's no need to further traverse children nodes, you can call
510/// `ctx.stop_traverse()` from inside a handler method, which will cancel
511/// further traverse.
512#[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}