Skip to content

Commit

Permalink
perf(linter): move shared context info to ContextHost
Browse files Browse the repository at this point in the history
  • Loading branch information
DonIsaac committed Sep 15, 2024
1 parent 213dbc0 commit f276c43
Show file tree
Hide file tree
Showing 67 changed files with 558 additions and 274 deletions.
171 changes: 171 additions & 0 deletions crates/oxc_linter/src/context/host.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
use oxc_semantic::Semantic;
use oxc_span::SourceType;
use std::{cell::RefCell, path::Path, rc::Rc, sync::Arc};

use crate::{
config::LintConfig,
disable_directives::{DisableDirectives, DisableDirectivesBuilder},
fixer::FixKind,
frameworks,
options::{LintOptions, LintPlugins},
utils, FrameworkFlags, RuleWithSeverity,
};

use super::{plugin_name_to_prefix, LintContext};

/// Stores shared information about a file being linted.
///
/// When linting a file, there are a number of shared resources that are
/// independent of the rule being linted. [`ContextHost`] stores this context
/// subset. When a lint rule is run, a [`LintContext`] with rule-specific
/// information is spawned using [`ContextHost::spawn`].
///
/// ## API Encapsulation
///
/// In most cases, lint rules should be interacting with a [`LintContext`]. The
/// only current exception to this is
/// [should_run](`crate::rule::Rule::should_run`). Just before a file is linted,
/// rules are filtered out based on that method. Creating a [`LintContext`] for
/// rules that would never get run is a waste of resources, so they must use
/// [`ContextHost`].
///
/// ## References
/// - [Flyweight Pattern](https://en.wikipedia.org/wiki/Flyweight_pattern)
pub struct ContextHost<'a> {
pub(super) semantic: Rc<Semantic<'a>>,
pub(super) disable_directives: DisableDirectives<'a>,
/// Whether or not to apply code fixes during linting. Defaults to
/// [`FixKind::None`] (no fixing).
///
/// Set via the `--fix`, `--fix-suggestions`, and `--fix-dangerously` CLI
/// flags.
pub(super) fix: FixKind,
pub(super) file_path: Box<Path>,
pub(super) config: Arc<LintConfig>,
pub(super) frameworks: FrameworkFlags,
pub(super) plugins: LintPlugins,
}

impl<'a> ContextHost<'a> {
/// # Panics
/// If `semantic.cfg()` is `None`.
pub fn new<P: AsRef<Path>>(
file_path: P,
semantic: Rc<Semantic<'a>>,
options: LintOptions,
) -> Self {
// We should always check for `semantic.cfg()` being `Some` since we depend on it and it is
// unwrapped without any runtime checks after construction.
assert!(
semantic.cfg().is_some(),
"`LintContext` depends on `Semantic::cfg`, Build your semantic with cfg enabled(`SemanticBuilder::with_cfg`)."
);

let disable_directives =
DisableDirectivesBuilder::new(semantic.source_text(), semantic.trivias().clone())
.build();

let file_path = file_path.as_ref().to_path_buf().into_boxed_path();

Self {
semantic,
disable_directives,
fix: options.fix,
file_path,
config: Arc::new(LintConfig::default()),
frameworks: options.framework_hints,
plugins: options.plugins,
}
.sniff_for_frameworks()
}

pub(crate) fn with_config(mut self, config: &Arc<LintConfig>) -> Self {
self.config = Arc::clone(config);
self
}

pub fn semantic(&self) -> &Semantic<'a> {
&self.semantic
}

/// Path to the file being linted.
///
/// When created from a [`LintService`](`crate::service::LintService`), this
/// will be an absolute path.
pub fn file_path(&self) -> &Path {
&self.file_path
}

/// The source type of the file being linted, e.g. JavaScript, TypeScript,
/// CJS, ESM, etc.
pub fn source_type(&self) -> &SourceType {
self.semantic.source_type()
}

pub(crate) fn spawn(self: Rc<Self>, rule: &RuleWithSeverity) -> LintContext<'a> {
const DIAGNOSTICS_INITIAL_CAPACITY: usize = 128;
let rule_name = rule.name();
let plugin_name = self.map_jest(rule.plugin_name(), rule_name);

LintContext {
parent: self,
diagnostics: RefCell::new(Vec::with_capacity(DIAGNOSTICS_INITIAL_CAPACITY)),
current_rule_name: rule_name,
current_plugin_name: plugin_name,
current_plugin_prefix: plugin_name_to_prefix(plugin_name),
#[cfg(debug_assertions)]
current_rule_fix_capabilities: rule.rule.fix(),
severity: rule.severity.into(),
}
}

#[cfg(test)]
pub(crate) fn spawn_for_test(self: Rc<Self>) -> LintContext<'a> {
const DIAGNOSTICS_INITIAL_CAPACITY: usize = 128;

LintContext {
parent: Rc::clone(&self),
diagnostics: RefCell::new(Vec::with_capacity(DIAGNOSTICS_INITIAL_CAPACITY)),
current_rule_name: "",
current_plugin_name: "eslint",
current_plugin_prefix: "eslint",
#[cfg(debug_assertions)]
current_rule_fix_capabilities: crate::rule::RuleFixMeta::None,
severity: oxc_diagnostics::Severity::Warning,
}
}

fn map_jest(&self, plugin_name: &'static str, rule_name: &str) -> &'static str {
if self.plugins.has_vitest()
&& plugin_name == "jest"
&& utils::is_jest_rule_adapted_to_vitest(rule_name)
{
"vitest"
} else {
plugin_name
}
}

/// Inspect the target file for clues about what frameworks are being used.
/// Should only be called once immediately after construction.
///
/// Before invocation, `self.frameworks` contains hints obtained at the
/// project level. For example, Oxlint may (eventually) search for a
/// `package.json`` and look for relevant dependencies. This method builds
/// on top of those hints, providing a more granular understanding of the
/// frameworks in use.
fn sniff_for_frameworks(mut self) -> Self {
if self.plugins.has_test() {
// let mut test_flags = FrameworkFlags::empty();

let vitest_like = frameworks::has_vitest_imports(self.semantic.module_record());
let jest_like = frameworks::is_jestlike_file(&self.file_path)
|| frameworks::has_jest_imports(self.semantic.module_record());

self.frameworks.set(FrameworkFlags::Vitest, vitest_like);
self.frameworks.set(FrameworkFlags::Jest, jest_like);
}

self
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#![allow(rustdoc::private_intra_doc_links)] // useful for intellisense
use std::{cell::RefCell, path::Path, rc::Rc, sync::Arc};
mod host;

use std::{cell::RefCell, path::Path, rc::Rc};

use oxc_cfg::ControlFlowGraph;
use oxc_diagnostics::{OxcDiagnostic, Severity};
Expand All @@ -10,36 +12,25 @@ use oxc_syntax::module_record::ModuleRecord;
#[cfg(debug_assertions)]
use crate::rule::RuleFixMeta;
use crate::{
config::LintConfig,
disable_directives::{DisableDirectives, DisableDirectivesBuilder},
disable_directives::DisableDirectives,
fixer::{FixKind, Message, RuleFix, RuleFixer},
javascript_globals::GLOBALS,
AllowWarnDeny, FrameworkFlags, OxlintEnv, OxlintGlobals, OxlintSettings,
};

pub use host::ContextHost;

#[derive(Clone)]
#[must_use]
pub struct LintContext<'a> {
semantic: Rc<Semantic<'a>>,
/// Shared context independent of the rule being linted.
parent: Rc<ContextHost<'a>>,

/// Diagnostics reported by the linter.
///
/// Contains diagnostics for all rules across all files.
diagnostics: RefCell<Vec<Message<'a>>>,

disable_directives: Rc<DisableDirectives<'a>>,

/// Whether or not to apply code fixes during linting. Defaults to
/// [`FixKind::None`] (no fixing).
///
/// Set via the `--fix`, `--fix-suggestions`, and `--fix-dangerously` CLI
/// flags.
fix: FixKind,

file_path: Rc<Path>,

config: Arc<LintConfig>,

// states
current_plugin_name: &'static str,
current_plugin_prefix: &'static str,
Expand All @@ -57,54 +48,11 @@ pub struct LintContext<'a> {
/// }
/// ```
severity: Severity,
frameworks: FrameworkFlags,
}

impl<'a> LintContext<'a> {
const WEBSITE_BASE_URL: &'static str = "https://oxc.rs/docs/guide/usage/linter/rules";

/// # Panics
/// If `semantic.cfg()` is `None`.
pub fn new(file_path: Box<Path>, semantic: Rc<Semantic<'a>>) -> Self {
const DIAGNOSTICS_INITIAL_CAPACITY: usize = 128;

// We should always check for `semantic.cfg()` being `Some` since we depend on it and it is
// unwrapped without any runtime checks after construction.
assert!(
semantic.cfg().is_some(),
"`LintContext` depends on `Semantic::cfg`, Build your semantic with cfg enabled(`SemanticBuilder::with_cfg`)."
);
let disable_directives =
DisableDirectivesBuilder::new(semantic.source_text(), semantic.trivias().clone())
.build();
Self {
semantic,
diagnostics: RefCell::new(Vec::with_capacity(DIAGNOSTICS_INITIAL_CAPACITY)),
disable_directives: Rc::new(disable_directives),
fix: FixKind::None,
file_path: file_path.into(),
config: Arc::new(LintConfig::default()),
current_plugin_name: "eslint",
current_plugin_prefix: "eslint",
current_rule_name: "",
#[cfg(debug_assertions)]
current_rule_fix_capabilities: RuleFixMeta::None,
severity: Severity::Warning,
frameworks: FrameworkFlags::empty(),
}
}

/// Enable/disable automatic code fixes.
pub fn with_fix(mut self, fix: FixKind) -> Self {
self.fix = fix;
self
}

pub(crate) fn with_config(mut self, config: &Arc<LintConfig>) -> Self {
self.config = Arc::clone(config);
self
}

pub fn with_plugin_name(mut self, plugin: &'static str) -> Self {
self.current_plugin_name = plugin;
self.current_plugin_prefix = plugin_name_to_prefix(plugin);
Expand All @@ -122,25 +70,19 @@ impl<'a> LintContext<'a> {
self
}

/// Update the severity of diagnostics reported by the rule this context is
/// associated with.
pub fn with_severity(mut self, severity: AllowWarnDeny) -> Self {
self.severity = Severity::from(severity);
self
}

/// Set [`FrameworkFlags`], overwriting any existing flags.
pub fn with_frameworks(mut self, frameworks: FrameworkFlags) -> Self {
self.frameworks = frameworks;
self
}

/// Add additional [`FrameworkFlags`]
pub fn and_frameworks(mut self, frameworks: FrameworkFlags) -> Self {
self.frameworks |= frameworks;
self
}

/// Get information such as the control flow graph, bound symbols, AST, etc.
/// for the file being linted.
///
/// Refer to [`Semantic`]'s documentation for more information.
pub fn semantic(&self) -> &Rc<Semantic<'a>> {
&self.semantic
&self.parent.semantic
}

pub fn cfg(&self) -> &ControlFlowGraph {
Expand All @@ -150,7 +92,7 @@ impl<'a> LintContext<'a> {
}

pub fn disable_directives(&self) -> &DisableDirectives<'a> {
&self.disable_directives
&self.parent.disable_directives
}

/// Source code of the file being linted.
Expand All @@ -171,23 +113,23 @@ impl<'a> LintContext<'a> {

/// Path to the file currently being linted.
pub fn file_path(&self) -> &Path {
&self.file_path
self.parent.file_path()
}

/// Plugin settings
pub fn settings(&self) -> &OxlintSettings {
&self.config.settings
&self.parent.config.settings
}

pub fn globals(&self) -> &OxlintGlobals {
&self.config.globals
&self.parent.config.globals
}

/// Runtime environments turned on/off by the user.
///
/// Examples of environments are `builtin`, `browser`, `node`, etc.
pub fn env(&self) -> &OxlintEnv {
&self.config.env
&self.parent.config.env
}

pub fn env_contains_var(&self, var: &str) -> bool {
Expand All @@ -211,7 +153,7 @@ impl<'a> LintContext<'a> {
}

fn add_diagnostic(&self, mut message: Message<'a>) {
if self.disable_directives.contains(self.current_rule_name, message.span()) {
if self.parent.disable_directives.contains(self.current_rule_name, message.span()) {
return;
}
message.error = message
Expand Down Expand Up @@ -334,7 +276,7 @@ impl<'a> LintContext<'a> {
(Some(message), None) => diagnostic.with_help(message.to_owned()),
_ => diagnostic,
};
if self.fix.can_apply(rule_fix.kind()) {
if self.parent.fix.can_apply(rule_fix.kind()) {
let fix = rule_fix.into_fix(self.source_text());
self.add_diagnostic(Message::new(diagnostic, Some(fix)));
} else {
Expand All @@ -343,7 +285,7 @@ impl<'a> LintContext<'a> {
}

pub fn frameworks(&self) -> FrameworkFlags {
self.frameworks
self.parent.frameworks
}

/// AST nodes
Expand Down Expand Up @@ -380,16 +322,6 @@ impl<'a> LintContext<'a> {
pub fn jsdoc(&self) -> &JSDocFinder<'a> {
self.semantic().jsdoc()
}

// #[inline]
// fn plugin_name_to_prefix(&self, plugin_name: &'static str) -> &'static str {
// let plugin_name = if self. plugin_name == "jest" && self.frameworks.contains(FrameworkFlags::Vitest) {
// "vitest"
// } else {
// plugin_name
// };
// PLUGIN_PREFIXES.get(plugin_name).copied().unwrap_or(plugin_name)
// }
}

#[inline]
Expand Down
Loading

0 comments on commit f276c43

Please sign in to comment.