Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion crates/oxc_linter/src/context/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ use super::{plugin_name_to_prefix, LintContext};
#[must_use]
#[non_exhaustive]
pub(crate) struct ContextHost<'a> {
/// Shared semantic information about the file being linted, which includes scopes, symbols
/// and AST nodes. See [`Semantic`].
pub(super) semantic: Rc<Semantic<'a>>,
/// Information about specific rules that should be disabled or enabled, via comment directives like
/// `eslint-disable` or `eslint-disable-next-line`.
pub(super) disable_directives: DisableDirectives<'a>,
/// Diagnostics reported by the linter.
///
Expand All @@ -46,9 +50,14 @@ pub(crate) struct ContextHost<'a> {
/// Set via the `--fix`, `--fix-suggestions`, and `--fix-dangerously` CLI
/// flags.
pub(super) fix: FixKind,
/// Path to the file being linted.
pub(super) file_path: Box<Path>,
/// Global linter configuration, such as globals to include and the target
/// environments, and other settings.
pub(super) config: Arc<LintConfig>,
/// Front-end frameworks that might be in use in the target file.
pub(super) frameworks: FrameworkFlags,
/// A list of all available linter plugins.
pub(super) plugins: LintPlugins,
}

Expand Down Expand Up @@ -88,12 +97,14 @@ impl<'a> ContextHost<'a> {
.sniff_for_frameworks()
}

/// Set the linter configuration for this context.
#[inline]
pub fn with_config(mut self, config: &Arc<LintConfig>) -> Self {
self.config = Arc::clone(config);
self
}

/// Shared reference to the [`Semantic`] analysis of the file.
#[inline]
pub fn semantic(&self) -> &Semantic<'a> {
&self.semantic
Expand All @@ -115,12 +126,14 @@ impl<'a> ContextHost<'a> {
self.semantic.source_type()
}

/// Add a diagnostic message to the end of the list of diagnostics. Can be used
/// by any rule to report issues.
#[inline]
pub(super) fn push_diagnostic(&self, diagnostic: Message<'a>) {
self.diagnostics.borrow_mut().push(diagnostic);
}

/// Take all diagnostics collected during linting.
/// Take ownership of all diagnostics collected during linting.
pub fn take_diagnostics(&self) -> Vec<Message<'a>> {
// NOTE: diagnostics are only ever borrowed here and in push_diagnostic.
// The latter drops the reference as soon as the function returns, so
Expand All @@ -129,6 +142,7 @@ impl<'a> ContextHost<'a> {
std::mem::take(&mut *messages)
}

/// Creates a new [`LintContext`] for a specific rule.
pub fn spawn(self: Rc<Self>, rule: &RuleWithSeverity) -> LintContext<'a> {
let rule_name = rule.name();
let plugin_name = self.map_jest(rule.plugin_name(), rule_name);
Expand All @@ -144,6 +158,7 @@ impl<'a> ContextHost<'a> {
}
}

/// Creates a new [`LintContext`] for testing purposes only.
#[cfg(test)]
pub(crate) fn spawn_for_test(self: Rc<Self>) -> LintContext<'a> {
LintContext {
Expand All @@ -157,6 +172,10 @@ impl<'a> ContextHost<'a> {
}
}

/// Maps Jest rule names and maps to Vitest rules when possible, returning the original plugin otherwise.
///
/// Many Vitest rules are essentially ports of the Jest plugin rules with minor modifications.
/// For these rules, we use the corresponding jest rules with some adjustments for compatibility.
fn map_jest(&self, plugin_name: &'static str, rule_name: &str) -> &'static str {
if self.plugins.has_vitest()
&& plugin_name == "jest"
Expand Down
47 changes: 44 additions & 3 deletions crates/oxc_linter/src/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,28 @@ pub(crate) use host::ContextHost;

#[derive(Clone)]
#[must_use]
/// Contains all of the state and context specific to this lint rule. Includes information
/// like the rule name, plugin name, and severity of the rule. It also has a reference to
/// the shared linting data [`ContextHost`], which is the same for all rules.
pub struct LintContext<'a> {
/// Shared context independent of the rule being linted.
parent: Rc<ContextHost<'a>>,

// states
/// Name of the plugin this rule belongs to. Example: `eslint`, `unicorn`, `react`
current_plugin_name: &'static str,
/// Prefixed version of the plugin name. Examples:
/// - `eslint-plugin-react`, for `react` plugin,
/// - `typescript-eslint`, for `typescript` plugin,
/// - `eslint-plugin-import`, for `import` plugin.
current_plugin_prefix: &'static str,
/// Kebab-cased name of the current rule being linted. Example: `no-unused-vars`, `no-undef`.
current_rule_name: &'static str,
/// Capabilities of the current rule to fix issues. Indicates whether:
/// - Rule cannot be auto-fixed [`RuleFixMeta::None`]
/// - Rule needs an auto-fix to be written still [`RuleFixMeta::FixPending`]
/// - Rule can be fixed in some cases [`RuleFixMeta::Conditional`]
/// - Rule is fully auto-fixable [`RuleFixMeta::Fixable`]
#[cfg(debug_assertions)]
current_rule_fix_capabilities: RuleFixMeta,

/// Current rule severity. Allows for user severity overrides, e.g.
/// ```json
/// // .oxlintrc.json
Expand All @@ -46,19 +57,23 @@ pub struct LintContext<'a> {
}

impl<'a> LintContext<'a> {
/// Base URL for the documentation, used to generate rule documentation URLs when a diagnostic is reported.
const WEBSITE_BASE_URL: &'static str = "https://oxc.rs/docs/guide/usage/linter/rules";

/// Set the plugin name for the current rule.
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);
self
}

/// Set the current rule name. Name should be kebab-cased like: `no-unused-vars` or `no-undef`.
pub fn with_rule_name(mut self, name: &'static str) -> Self {
self.current_rule_name = name;
self
}

/// Set the current rule fix capabilities. See [`RuleFixMeta`] for more information.
#[cfg(debug_assertions)]
pub fn with_rule_fix_capabilities(mut self, capabilities: RuleFixMeta) -> Self {
self.current_rule_fix_capabilities = capabilities;
Expand All @@ -82,13 +97,15 @@ impl<'a> LintContext<'a> {
&self.parent.semantic
}

/// Get the control flow graph for the current program.
#[inline]
pub fn cfg(&self) -> &ControlFlowGraph {
// SAFETY: `LintContext::new` is the only way to construct a `LintContext` and we always
// assert the existence of control flow so it should always be `Some`.
unsafe { self.semantic().cfg().unwrap_unchecked() }
}

/// List of all disable directives in the file being linted.
#[inline]
pub fn disable_directives(&self) -> &DisableDirectives<'a> {
&self.parent.disable_directives
Expand Down Expand Up @@ -124,6 +141,7 @@ impl<'a> LintContext<'a> {
&self.parent.config.settings
}

/// Sets of global variables that have been enabled or disabled.
#[inline]
pub fn globals(&self) -> &OxlintGlobals {
&self.parent.config.globals
Expand All @@ -137,6 +155,12 @@ impl<'a> LintContext<'a> {
&self.parent.config.env
}

/// Checks if a given variable named is defined as a global variable in the current environment.
///
/// Example:
/// - `env_contains_var("Date")` returns `true` because it is a global builtin in all environments.
/// - `env_contains_var("HTMLElement")` returns `true` only if the `browser` environment is enabled.
/// - `env_contains_var("globalThis")` returns `true` only if the `es2020` environment or higher is enabled.
pub fn env_contains_var(&self, var: &str) -> bool {
if GLOBALS["builtin"].contains_key(var) {
return true;
Expand All @@ -153,6 +177,8 @@ impl<'a> LintContext<'a> {

/* Diagnostics */

/// Add a diagnostic message to the list of diagnostics. Outputs a diagnostic with the current rule
/// name, severity, and a link to the rule's documentation URL.
fn add_diagnostic(&self, mut message: Message<'a>) {
if self.parent.disable_directives.contains(self.current_rule_name, message.span()) {
return;
Expand Down Expand Up @@ -251,6 +277,12 @@ impl<'a> LintContext<'a> {
self.diagnostic_with_fix_of_kind(diagnostic, FixKind::DangerousFix, fix);
}

/// Report a lint rule violation and provide an automatic fix of a specific kind.
///
/// The second argument is a [closure] that takes a [`RuleFixer`] and
/// returns something that can turn into a [`RuleFix`].
///
/// [closure]: <https://doc.rust-lang.org/book/ch13-01-closures.html>
#[allow(clippy::missing_panics_doc)] // only panics in debug mode
pub fn diagnostic_with_fix_of_kind<C, F>(
&self,
Expand Down Expand Up @@ -285,6 +317,7 @@ impl<'a> LintContext<'a> {
}
}

/// Framework flags, indicating front-end frameworks that might be in use.
pub fn frameworks(&self) -> FrameworkFlags {
self.parent.frameworks
}
Expand Down Expand Up @@ -325,11 +358,19 @@ impl<'a> LintContext<'a> {
}
}

/// Gets the prefixed plugin name, given the short plugin name.
///
/// Example:
///
/// ```rust
/// assert_eq!(plugin_name_to_prefix("react"), "eslint-plugin-react");
/// ```
#[inline]
fn plugin_name_to_prefix(plugin_name: &'static str) -> &'static str {
PLUGIN_PREFIXES.get(plugin_name).copied().unwrap_or(plugin_name)
}

/// Map of plugin names to their prefixed versions.
const PLUGIN_PREFIXES: phf::Map<&'static str, &'static str> = phf::phf_map! {
"import" => "eslint-plugin-import",
"jest" => "eslint-plugin-jest",
Expand Down