use crate::control_flow::ControlFlow;
use crate::diagnostic::{
LintDiagnostic, LintDiagnosticDetails, LintDiagnosticRange, LintDocsUrl,
LintFix,
};
use crate::ignore_directives::{
parse_line_ignore_directives, CodeStatus, FileIgnoreDirective,
LineIgnoreDirective,
};
use crate::linter::LinterContext;
use crate::rules;
use deno_ast::swc::ast::Expr;
use deno_ast::swc::common::comments::Comment;
use deno_ast::swc::common::util::take::Take;
use deno_ast::swc::common::{SourceMap, SyntaxContext};
use deno_ast::SourceTextInfo;
use deno_ast::{
view as ast_view, ParsedSource, RootNode, SourcePos, SourceRange,
};
use deno_ast::{MediaType, ModuleSpecifier};
use deno_ast::{MultiThreadedComments, Scope};
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
pub struct Context<'a> {
parsed_source: ParsedSource,
diagnostics: Vec<LintDiagnostic>,
program: ast_view::Program<'a>,
file_ignore_directive: Option<FileIgnoreDirective>,
line_ignore_directives: HashMap<usize, LineIgnoreDirective>,
scope: Scope,
control_flow: ControlFlow,
traverse_flow: TraverseFlow,
check_unknown_rules: bool,
#[allow(clippy::redundant_allocation)] jsx_factory: Option<Rc<Box<Expr>>>,
#[allow(clippy::redundant_allocation)] jsx_fragment_factory: Option<Rc<Box<Expr>>>,
}
impl<'a> Context<'a> {
pub(crate) fn new(
linter_ctx: &'a LinterContext,
parsed_source: ParsedSource,
program: ast_view::Program<'a>,
file_ignore_directive: Option<FileIgnoreDirective>,
default_jsx_factory: Option<String>,
default_jsx_fragment_factory: Option<String>,
) -> Self {
let line_ignore_directives = parse_line_ignore_directives(
linter_ctx.ignore_diagnostic_directive,
program,
);
let scope = Scope::analyze(program);
let control_flow =
ControlFlow::analyze(program, parsed_source.unresolved_context());
let mut jsx_factory = None;
let mut jsx_fragment_factory = None;
parsed_source.globals().with(|marks| {
let top_level_mark = marks.top_level;
if let Some(leading_comments) = parsed_source.get_leading_comments() {
let jsx_directives =
deno_ast::swc::transforms::react::JsxDirectives::from_comments(
&SourceMap::default(),
#[allow(clippy::disallowed_types)]
deno_ast::swc::common::Span::dummy(),
leading_comments,
top_level_mark,
);
jsx_factory = jsx_directives.pragma;
jsx_fragment_factory = jsx_directives.pragma_frag;
}
if jsx_factory.is_none() {
if let Some(factory) = default_jsx_factory {
jsx_factory = Some(Rc::new(
deno_ast::swc::transforms::react::parse_expr_for_jsx(
&SourceMap::default(),
"jsx",
factory.into(),
top_level_mark,
),
));
}
}
if jsx_fragment_factory.is_none() {
if let Some(factory) = default_jsx_fragment_factory {
jsx_fragment_factory = Some(Rc::new(
deno_ast::swc::transforms::react::parse_expr_for_jsx(
&SourceMap::default(),
"jsxFragment",
factory.into(),
top_level_mark,
),
));
}
}
});
Self {
file_ignore_directive,
line_ignore_directives,
scope,
control_flow,
program,
parsed_source,
diagnostics: Vec::new(),
traverse_flow: TraverseFlow::default(),
check_unknown_rules: linter_ctx.check_unknown_rules,
jsx_factory,
jsx_fragment_factory,
}
}
pub fn specifier(&self) -> &ModuleSpecifier {
self.parsed_source.specifier()
}
pub fn media_type(&self) -> MediaType {
self.parsed_source.media_type()
}
pub fn comments(&self) -> &MultiThreadedComments {
self.parsed_source.comments()
}
pub fn diagnostics(&self) -> &[LintDiagnostic] {
&self.diagnostics
}
pub fn parsed_source(&self) -> &ParsedSource {
&self.parsed_source
}
pub fn text_info(&self) -> &SourceTextInfo {
self.parsed_source.text_info_lazy()
}
pub fn program(&self) -> ast_view::Program<'a> {
self.program
}
pub fn file_ignore_directive(&self) -> Option<&FileIgnoreDirective> {
self.file_ignore_directive.as_ref()
}
pub fn line_ignore_directives(&self) -> &HashMap<usize, LineIgnoreDirective> {
&self.line_ignore_directives
}
pub fn scope(&self) -> &Scope {
&self.scope
}
pub fn control_flow(&self) -> &ControlFlow {
&self.control_flow
}
pub fn jsx_factory(&self) -> Option<Rc<Box<Expr>>> {
self.jsx_factory.clone()
}
pub fn jsx_fragment_factory(&self) -> Option<Rc<Box<Expr>>> {
self.jsx_fragment_factory.clone()
}
pub(crate) fn unresolved_ctxt(&self) -> SyntaxContext {
self.parsed_source.unresolved_context()
}
pub(crate) fn assert_traverse_init(&self) {
self.traverse_flow.assert_init();
}
pub(crate) fn should_stop_traverse(&mut self) -> bool {
self.traverse_flow.should_stop()
}
pub(crate) fn stop_traverse(&mut self) {
self.traverse_flow.set_stop_traverse();
}
pub fn all_comments(&self) -> impl Iterator<Item = &'a Comment> {
self.program.comment_container().all_comments()
}
pub fn leading_comments_at(
&self,
start: SourcePos,
) -> impl Iterator<Item = &'a Comment> {
self.program.comment_container().leading_comments(start)
}
pub fn trailing_comments_at(
&self,
end: SourcePos,
) -> impl Iterator<Item = &'a Comment> {
self.program.comment_container().trailing_comments(end)
}
pub(crate) fn check_ignore_directive_usage(&mut self) -> Vec<LintDiagnostic> {
let mut filtered = Vec::new();
for diagnostic in self.diagnostics.iter().cloned() {
if let Some(f) = self.file_ignore_directive.as_mut() {
if f.check_used(&diagnostic.details.code) {
continue;
}
}
let Some(range) = diagnostic.range.as_ref() else {
continue;
};
let diagnostic_line = range.text_info.line_index(range.range.start);
if diagnostic_line > 0 {
if let Some(l) =
self.line_ignore_directives.get_mut(&(diagnostic_line - 1))
{
if l.check_used(&diagnostic.details.code) {
continue;
}
}
}
filtered.push(diagnostic);
}
filtered
}
pub(crate) fn ban_unused_ignore(
&self,
known_rules_codes: &HashSet<Cow<'static, str>>,
) -> Vec<LintDiagnostic> {
const CODE: &str = "ban-unused-ignore";
if self
.file_ignore_directive
.as_ref()
.is_some_and(|file_ignore| file_ignore.has_code(CODE))
{
return vec![];
}
let is_unused_code = |&(code, status): &(&String, &CodeStatus)| {
let is_unknown = !known_rules_codes.contains(code.as_str());
!status.used && !is_unknown
};
let mut diagnostics = Vec::new();
if let Some(file_ignore) = self.file_ignore_directive.as_ref() {
for (unused_code, _status) in
file_ignore.codes().iter().filter(is_unused_code)
{
let d = self.create_diagnostic(
Some(self.create_diagnostic_range(file_ignore.range())),
self.create_diagnostic_details(
CODE,
format!("Ignore for code \"{}\" was not used.", unused_code),
None,
Vec::new(),
),
);
diagnostics.push(d);
}
}
for line_ignore in self.line_ignore_directives.values() {
for (unused_code, _status) in
line_ignore.codes().iter().filter(is_unused_code)
{
let d = self.create_diagnostic(
Some(self.create_diagnostic_range(line_ignore.range())),
self.create_diagnostic_details(
CODE,
format!("Ignore for code \"{}\" was not used.", unused_code),
None,
Vec::new(),
),
);
diagnostics.push(d);
}
}
diagnostics
}
pub(crate) fn ban_unknown_rule_code(
&mut self,
enabled_rules: &HashSet<Cow<'static, str>>,
) -> Vec<LintDiagnostic> {
let mut diagnostics = Vec::new();
if let Some(file_ignore) = self.file_ignore_directive.as_ref() {
for unknown_rule_code in file_ignore
.codes()
.keys()
.filter(|code| !enabled_rules.contains(code.as_str()))
{
let d = self.create_diagnostic(
Some(self.create_diagnostic_range(file_ignore.range())),
self.create_diagnostic_details(
rules::ban_unknown_rule_code::CODE,
format!("Unknown rule for code \"{}\"", unknown_rule_code),
None,
Vec::new(),
),
);
diagnostics.push(d);
}
}
for line_ignore in self.line_ignore_directives.values() {
for unknown_rule_code in line_ignore
.codes()
.keys()
.filter(|code| !enabled_rules.contains(code.as_str()))
{
let d = self.create_diagnostic(
Some(self.create_diagnostic_range(line_ignore.range())),
self.create_diagnostic_details(
rules::ban_unknown_rule_code::CODE,
format!("Unknown rule for code \"{}\"", unknown_rule_code),
None,
Vec::new(),
),
);
diagnostics.push(d);
}
}
if !diagnostics.is_empty() {
if let Some(f) = self.file_ignore_directive.as_mut() {
f.check_used(rules::ban_unknown_rule_code::CODE);
}
}
if self.check_unknown_rules
&& !self
.file_ignore_directive()
.map(|f| f.has_code(rules::ban_unknown_rule_code::CODE))
.unwrap_or(false)
{
diagnostics
} else {
vec![]
}
}
pub fn add_diagnostic(
&mut self,
range: SourceRange,
code: impl ToString,
message: impl ToString,
) {
self.add_diagnostic_details(
Some(self.create_diagnostic_range(range)),
self.create_diagnostic_details(
code,
message.to_string(),
None,
Vec::new(),
),
);
}
pub fn add_diagnostic_with_hint(
&mut self,
range: SourceRange,
code: impl ToString,
message: impl ToString,
hint: impl ToString,
) {
self.add_diagnostic_details(
Some(self.create_diagnostic_range(range)),
self.create_diagnostic_details(
code,
message,
Some(hint.to_string()),
Vec::new(),
),
);
}
pub fn add_diagnostic_with_fixes(
&mut self,
range: SourceRange,
code: impl ToString,
message: impl ToString,
hint: Option<String>,
fixes: Vec<LintFix>,
) {
self.add_diagnostic_details(
Some(self.create_diagnostic_range(range)),
self.create_diagnostic_details(code, message, hint, fixes),
);
}
pub fn add_diagnostic_details(
&mut self,
maybe_range: Option<LintDiagnosticRange>,
details: LintDiagnosticDetails,
) {
self
.diagnostics
.push(self.create_diagnostic(maybe_range, details));
}
pub fn add_external_diagnostics(&mut self, diagnostics: &[LintDiagnostic]) {
self.diagnostics.extend_from_slice(diagnostics);
}
pub(crate) fn create_diagnostic(
&self,
maybe_range: Option<LintDiagnosticRange>,
details: LintDiagnosticDetails,
) -> LintDiagnostic {
LintDiagnostic {
specifier: self.specifier().clone(),
range: maybe_range,
details,
}
}
pub(crate) fn create_diagnostic_details(
&self,
code: impl ToString,
message: impl ToString,
maybe_hint: Option<String>,
fixes: Vec<LintFix>,
) -> LintDiagnosticDetails {
LintDiagnosticDetails {
message: message.to_string(),
code: code.to_string(),
hint: maybe_hint,
fixes,
custom_docs_url: LintDocsUrl::Default,
info: vec![],
}
}
pub(crate) fn create_diagnostic_range(
&self,
range: SourceRange,
) -> LintDiagnosticRange {
LintDiagnosticRange {
range,
text_info: self.text_info().clone(),
description: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
struct TraverseFlow {
stop_traverse: bool,
}
impl TraverseFlow {
fn set_stop_traverse(&mut self) {
self.stop_traverse = true;
}
fn reset(&mut self) {
self.stop_traverse = false;
}
fn assert_init(&self) {
assert!(!self.stop_traverse);
}
fn should_stop(&mut self) -> bool {
let stop = self.stop_traverse;
self.reset();
stop
}
}