feat(linter): support read env from eslintrc (#2130)

## target

resolve #732 

## contexts

- globals definition from
https://github.com/sindresorhus/globals/blob/main/globals.json
- port https://github.com/eslint/eslintrc/blob/main/conf/environments.js
to derive environments
This commit is contained in:
fi3ework 2024-01-26 10:43:44 +08:00 committed by GitHub
parent e5719e9b4d
commit ee5b9682ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 2237 additions and 94 deletions

10
Cargo.lock generated
View file

@ -915,6 +915,16 @@ version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "javascript_globals"
version = "0.0.0"
dependencies = [
"handlebars",
"lazy_static",
"oxc_tasks_common",
"serde",
]
[[package]]
name = "jemalloc-sys"
version = "0.5.4+5.3.0-patched"

View file

@ -0,0 +1,8 @@
{
"env": {
"browser": true
},
"rules": {
"no-undef": "error"
}
}

View file

@ -0,0 +1,5 @@
{
"rules": {
"no-undef": "error"
}
}

View file

@ -0,0 +1 @@
console.log('')

View file

@ -324,6 +324,29 @@ mod test {
assert_eq!(result.number_of_errors, 0);
}
#[test]
fn eslintrc_no_env() {
let args =
&["-c", "fixtures/eslintrc_env/eslintrc_no_env.json", "fixtures/eslintrc_env/test.js"];
let result = test(args);
assert_eq!(result.number_of_files, 1);
assert_eq!(result.number_of_warnings, 1);
assert_eq!(result.number_of_errors, 0);
}
#[test]
fn eslintrc_with_env() {
let args = &[
"-c",
"fixtures/eslintrc_env/eslintrc_env_browser.json",
"fixtures/eslintrc_env/test.js",
];
let result = test(args);
assert_eq!(result.number_of_files, 1);
assert_eq!(result.number_of_warnings, 0);
assert_eq!(result.number_of_errors, 0);
}
#[test]
fn no_empty_allow_empty_catch() {
let args = &[

View file

@ -5,7 +5,7 @@ use oxc_diagnostics::{Error, FailedToOpenFileError, Report};
use rustc_hash::{FxHashMap, FxHashSet};
use serde_json::Value;
use crate::{rules::RuleEnum, settings::Nextjs, AllowWarnDeny, JsxA11y, LintSettings};
use crate::{rules::RuleEnum, settings::Nextjs, AllowWarnDeny, Env, JsxA11y, LintSettings};
use self::errors::{
FailedToParseConfigError, FailedToParseConfigJsonError, FailedToParseJsonc,
@ -15,6 +15,7 @@ use self::errors::{
pub struct ESLintConfig {
rules: Vec<ESLintRuleConfig>,
settings: LintSettings,
env: Env,
}
#[derive(Debug)]
@ -30,11 +31,12 @@ impl ESLintConfig {
let json = Self::read_json(path)?;
let rules = parse_rules(&json)?;
let settings = parse_settings_from_root(&json);
Ok(Self { rules, settings })
let env = parse_env_from_root(&json);
Ok(Self { rules, settings, env })
}
pub fn settings(self) -> LintSettings {
self.settings
pub fn properties(self) -> (LintSettings, Env) {
(self.settings, self.env)
}
fn read_json(path: &Path) -> Result<serde_json::Value, Error> {
@ -201,6 +203,28 @@ pub fn parse_settings(setting_value: &Value) -> LintSettings {
LintSettings::default()
}
fn parse_env_from_root(root_json: &Value) -> Env {
let Value::Object(root_object) = root_json else { return Env::default() };
let Some(env_value) = root_object.get("env") else { return Env::default() };
let env_object = match env_value {
Value::Object(env_object) => env_object,
_ => return Env::default(),
};
let mut result = vec![];
for (k, v) in env_object {
if let Value::Bool(v) = v {
if *v {
result.push(String::from(k));
}
}
}
Env::new(result)
}
fn parse_rule_name(name: &str) -> (&str, &str) {
if let Some((category, name)) = name.split_once('/') {
let category = category.trim_start_matches('@');

View file

@ -8,7 +8,8 @@ use oxc_span::SourceType;
use crate::{
disable_directives::{DisableDirectives, DisableDirectivesBuilder},
fixer::{Fix, Message},
AstNode, LintSettings,
javascript_globals::GLOBALS,
AstNode, Env, LintSettings,
};
pub struct LintContext<'a> {
@ -26,6 +27,8 @@ pub struct LintContext<'a> {
file_path: Box<Path>,
settings: Arc<LintSettings>,
env: Arc<Env>,
}
impl<'a> LintContext<'a> {
@ -40,6 +43,7 @@ impl<'a> LintContext<'a> {
current_rule_name: "",
file_path,
settings: Arc::new(LintSettings::default()),
env: Arc::new(Env::default()),
}
}
@ -55,6 +59,12 @@ impl<'a> LintContext<'a> {
self
}
#[must_use]
pub fn with_env(mut self, env: &Arc<Env>) -> Self {
self.env = Arc::clone(env);
self
}
pub fn semantic(&self) -> &Rc<Semantic<'a>> {
&self.semantic
}
@ -79,6 +89,21 @@ impl<'a> LintContext<'a> {
&self.file_path
}
pub fn envs(&self) -> &Env {
&self.env
}
pub fn env_contains_var(&self, var: &str) -> bool {
for env in self.env.iter() {
let env = GLOBALS.get(env).unwrap_or(&GLOBALS["builtin"]);
if env.get(var).is_some() {
return true;
}
}
false
}
#[inline]
pub fn with_rule_name(&mut self, name: &'static str) {
self.current_rule_name = name;

View file

@ -0,0 +1,25 @@
use std::{self, ops::Deref};
#[derive(Debug, Clone)]
pub struct Env(Vec<String>);
impl Env {
pub fn new(env: Vec<String>) -> Self {
Self(env)
}
}
/// The `env` field from ESLint config
impl Default for Env {
fn default() -> Self {
Self(vec!["builtin".to_string()])
}
}
impl Deref for Env {
type Target = Vec<String>;
fn deref(&self) -> &Self::Target {
&self.0
}
}

View file

@ -1,79 +1,5 @@
//! [Globals](https://github.com/sindresorhus/globals/blob/main/globals.json)
//! Each global is given a value of true or false.
//! A value of true indicates that the variable may be overwritten.
//! A value of false indicates that the variable should be considered read-only.
use phf::{phf_map, phf_set, Map};
pub const BUILTINS: Map<&'static str, bool> = phf_map! {
"AggregateError" => false,
"Array" => false,
"ArrayBuffer" => false,
"Atomics" => false,
"BigInt" => false,
"BigInt64Array" => false,
"BigUint64Array" => false,
"Boolean" => false,
"constructor" => false,
"DataView" => false,
"Date" => false,
"decodeURI" => false,
"decodeURIComponent" => false,
"encodeURI" => false,
"encodeURIComponent" => false,
"Error" => false,
"escape" => false,
"eval" => false,
"EvalError" => false,
"FinalizationRegistry" => false,
"Float32Array" => false,
"Float64Array" => false,
"Function" => false,
"globalThis" => false,
"hasOwnProperty" => false,
"Infinity" => false,
"Int16Array" => false,
"Int32Array" => false,
"Int8Array" => false,
"isFinite" => false,
"isNaN" => false,
"isPrototypeOf" => false,
"JSON" => false,
"Map" => false,
"Math" => false,
"NaN" => false,
"Number" => false,
"Object" => false,
"parseFloat" => false,
"parseInt" => false,
"Promise" => false,
"propertyIsEnumerable" => false,
"Proxy" => false,
"RangeError" => false,
"ReferenceError" => false,
"Reflect" => false,
"RegExp" => false,
"Set" => false,
"SharedArrayBuffer" => false,
"String" => false,
"Symbol" => false,
"Diagnostic" => false,
"toLocaleString" => false,
"toString" => false,
"TypeError" => false,
"Uint16Array" => false,
"Uint32Array" => false,
"Uint8Array" => false,
"Uint8ClampedArray" => false,
"undefined" => false,
"unescape" => false,
"URIError" => false,
"valueOf" => false,
"WeakMap" => false,
"WeakRef" => false,
"WeakSet" => false
};
pub const PRE_DEFINE_VAR: Map<&'static str, bool> = phf_map! {
"undefined" => false,
"Infinity" => false,

File diff suppressed because it is too large Load diff

View file

@ -8,8 +8,10 @@ mod ast_util;
mod config;
mod context;
mod disable_directives;
mod env;
mod fixer;
mod globals;
mod javascript_globals;
mod options;
pub mod partial_loader;
pub mod rule;
@ -25,6 +27,7 @@ use oxc_diagnostics::Report;
pub use crate::{
context::LintContext,
env::Env,
fixer::Fix,
fixer::{FixResult, Fixer, Message},
options::{AllowWarnDeny, LintOptions},
@ -52,6 +55,7 @@ pub struct Linter {
rules: Vec<(/* rule name */ &'static str, RuleEnum)>,
options: LintOptions,
settings: Arc<LintSettings>,
env: Arc<Env>,
}
impl Default for Linter {
@ -65,9 +69,9 @@ impl Linter {
///
/// Returns `Err` if there are any errors parsing the configuration file.
pub fn from_options(options: LintOptions) -> Result<Self, Report> {
let (rules, settings) = options.derive_rules_and_settings()?;
let (rules, settings, env) = options.derive_rules_and_settings_and_env()?;
let rules = rules.into_iter().map(|rule| (rule.name(), rule)).collect();
Ok(Self { rules, options, settings: Arc::new(settings) })
Ok(Self { rules, options, settings: Arc::new(settings), env: Arc::new(env) })
}
#[must_use]
@ -82,6 +86,12 @@ impl Linter {
self
}
#[must_use]
pub fn with_envs(mut self, env: Env) -> Self {
self.env = Arc::new(env);
self
}
pub fn options(&self) -> &LintOptions {
&self.options
}
@ -98,7 +108,8 @@ impl Linter {
pub fn run<'a>(&self, ctx: LintContext<'a>) -> Vec<Message<'a>> {
let semantic = Rc::clone(ctx.semantic());
let mut ctx = ctx.with_fix(self.options.fix).with_settings(&self.settings);
let mut ctx =
ctx.with_fix(self.options.fix).with_settings(&self.settings).with_env(&self.env);
for (rule_name, rule) in &self.rules {
ctx.with_rule_name(rule_name);

View file

@ -9,7 +9,7 @@ use crate::{
ESLintConfig,
},
rules::RULES,
LintSettings, RuleCategory, RuleEnum,
Env, LintSettings, RuleCategory, RuleEnum,
};
use oxc_diagnostics::Error;
use rustc_hash::FxHashSet;
@ -28,6 +28,7 @@ pub struct LintOptions {
pub jsx_a11y_plugin: bool,
pub nextjs_plugin: bool,
pub react_perf_plugin: bool,
pub env: Env,
}
impl Default for LintOptions {
@ -42,6 +43,7 @@ impl Default for LintOptions {
jsx_a11y_plugin: false,
nextjs_plugin: false,
react_perf_plugin: false,
env: Env::default(),
}
}
}
@ -102,6 +104,12 @@ impl LintOptions {
self.react_perf_plugin = yes;
self
}
#[must_use]
pub fn with_env(mut self, env: Vec<String>) -> Self {
self.env = Env::new(env);
self
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
@ -168,7 +176,9 @@ impl LintOptions {
/// # Errors
///
/// * Returns `Err` if there are any errors parsing the configuration file.
pub fn derive_rules_and_settings(&self) -> Result<(Vec<RuleEnum>, LintSettings), Error> {
pub fn derive_rules_and_settings_and_env(
&self,
) -> Result<(Vec<RuleEnum>, LintSettings, Env), Error> {
let config = self.config_path.as_ref().map(|path| ESLintConfig::new(path)).transpose()?;
let mut rules: FxHashSet<RuleEnum> = FxHashSet::default();
@ -216,10 +226,13 @@ impl LintOptions {
}
let mut rules = rules.into_iter().collect::<Vec<_>>();
let (settings, env) = config.map(ESLintConfig::properties).unwrap_or_default();
// for stable diagnostics output ordering
rules.sort_unstable_by_key(RuleEnum::name);
Ok((rules, config.map(ESLintConfig::settings).unwrap_or_default()))
Ok((rules, settings, env))
}
// get final filtered rules by reading `self.jest_plugin` and `self.jsx_a11y_plugin`

View file

@ -11,7 +11,6 @@ use oxc_syntax::operator::{AssignmentOperator, BinaryOperator, LogicalOperator,
use crate::{
ast_util::{self, IsConstant},
context::LintContext,
globals::BUILTINS,
rule::Rule,
AstNode,
};
@ -349,7 +348,7 @@ impl NoConstantBinaryExpression {
| Expression::RegExpLiteral(_) => true,
Expression::NewExpression(call_expr) => {
if let Expression::Identifier(ident) = &call_expr.callee {
return BUILTINS.contains_key(ident.name.as_str())
return ctx.env_contains_var(ident.name.as_str())
&& ctx.semantic().is_reference_to_global_variable(ident);
}
false

View file

@ -5,7 +5,7 @@ use oxc_diagnostics::{
use oxc_macros::declare_oxc_lint;
use oxc_span::{Atom, Span};
use crate::{context::LintContext, globals::BUILTINS, rule::Rule};
use crate::{context::LintContext, rule::Rule};
#[derive(Debug, Error, Diagnostic)]
#[error("eslint(no-global-assign): Read-only global '{0}' should not be modified.")]
@ -71,7 +71,7 @@ impl Rule for NoGlobalAssign {
if reference.is_write() && symbol_table.is_global_reference(reference_id) {
let name = reference.name();
if !self.excludes.contains(name) && BUILTINS.contains_key(name) {
if !self.excludes.contains(name) && ctx.env_contains_var(name) {
ctx.diagnostic(NoGlobalAssignDiagnostic(name.clone(), reference.span()));
}
}

View file

@ -10,7 +10,7 @@ use oxc_macros::declare_oxc_lint;
use oxc_semantic::VariableInfo;
use oxc_span::{Atom, Span};
use crate::{context::LintContext, globals::BUILTINS, rule::Rule};
use crate::{context::LintContext, rule::Rule};
#[derive(Debug, Error, Diagnostic)]
#[error("eslint(no-redeclare): '{0}' is already defined.")]
@ -106,7 +106,7 @@ impl NoRedeclare {
variable: &VariableInfo,
ident: &BindingIdentifier,
) {
if self.built_in_globals && BUILTINS.get(&ident.name).is_some() {
if self.built_in_globals && ctx.env_contains_var(&ident.name) {
ctx.diagnostic(NoRedeclareAsBuiltiInDiagnostic(ident.name.clone(), ident.span));
} else if variable.span != ident.span {
ctx.diagnostic(NoRedeclareDiagnostic(ident.name.clone(), ident.span, variable.span));

View file

@ -7,7 +7,7 @@ use oxc_macros::declare_oxc_lint;
use oxc_span::{Atom, Span};
use oxc_syntax::operator::UnaryOperator;
use crate::{context::LintContext, globals::BUILTINS, rule::Rule, AstNode};
use crate::{context::LintContext, rule::Rule, AstNode};
#[derive(Debug, Error, Diagnostic)]
#[error("eslint(no-undef): Disallow the use of undeclared variables")]
@ -35,7 +35,7 @@ declare_oxc_lint!(
/// var bar = a + 1;
/// ```
NoUndef,
nursery // https://github.com/oxc-project/oxc/issues/732
nursery
);
impl Rule for NoUndef {
@ -53,7 +53,7 @@ impl Rule for NoUndef {
for reference_id_list in ctx.scopes().root_unresolved_references().values() {
for &reference_id in reference_id_list {
let reference = symbol_table.get_reference(reference_id);
if BUILTINS.contains_key(reference.name().as_str()) {
if ctx.env_contains_var(reference.name().as_str()) {
return;
}

View file

@ -74,6 +74,10 @@ codecov:
benchmark:
cargo benchmark
# Generate the JavaScript global variables. See `tasks/javascript_globals`
javascript-globals:
cargo run -p javascript_globals
# Create a new lint rule by providing the ESLint name. See `tasks/rulegen`
new-rule name:
cargo run -p rulegen {{name}}

View file

@ -0,0 +1,21 @@
[package]
name = "javascript_globals"
version = "0.0.0"
publish = false
edition.workspace = true
license.workspace = true
[lints]
workspace = true
[[bin]]
name = "javascript_globals"
test = false
[dependencies]
oxc_tasks_common = { workspace = true }
serde = { workspace = true, features = ["derive"] }
lazy_static = { workspace = true }
handlebars = "5.0.0"

View file

@ -0,0 +1,167 @@
use lazy_static::lazy_static;
use oxc_tasks_common::agent;
use serde::Serialize;
use std::collections::HashMap;
mod template;
#[derive(Serialize, Debug)]
pub struct EnvVar<'a> {
pub name: &'a str,
pub writeable: bool,
}
#[derive(Serialize, Debug)]
pub struct Env<'a> {
pub name: &'a str,
pub vars: Vec<EnvVar<'a>>,
}
#[derive(Serialize)]
pub struct Context<'a> {
envs: Vec<Env<'a>>,
}
impl<'a> Context<'a> {
fn new(envs: Vec<Env<'a>>) -> Self {
Self { envs }
}
}
fn get_diff(
current: &HashMap<String, bool>,
prev: &HashMap<String, bool>,
) -> HashMap<String, bool> {
let mut retv: HashMap<String, bool> = HashMap::new();
for (key, value) in current {
if !prev.contains_key(key) {
retv.insert(key.clone(), *value);
}
}
retv
}
lazy_static! {
static ref NEW_GLOBALS_2017: HashMap<String, bool> = {
return HashMap::from([
(String::from("Atomics"), false),
(String::from("SharedArrayBuffer"), false),
]);
};
static ref NEW_GLOBALS_2020: HashMap<String, bool> = {
return HashMap::from([
(String::from("BigInt"), false),
(String::from("BigInt64Array"), false),
(String::from("BigUint64Array"), false),
(String::from("globalThis"), false),
]);
};
static ref NEW_GLOBALS_2021: HashMap<String, bool> = {
return HashMap::from([
(String::from("AggregateError"), false),
(String::from("FinalizationRegistry"), false),
(String::from("WeakRef"), false),
]);
};
}
fn main() {
// Each global is given a value of true or false.
// A value of true indicates that the variable may be overwritten.
// A value of false indicates that the variable should be considered read-only.
// open globals.json file relative to current file
// let globals: HashMap<String, HashMap<String, bool>>;
let globals: HashMap<String, HashMap<String, bool>> = match agent()
.get("https://raw.githubusercontent.com/sindresorhus/globals/main/globals.json")
.call()
{
Ok(response) => response.into_json().unwrap(),
Err(e) => {
panic!("Failed to fetch globals.json: {e}");
}
};
// 19 variables such as Promise, Map, ...
let new_globals_2015 = get_diff(&globals["es2015"], &globals["es5"]);
let new_globals_2015_2017 = {
let mut map = HashMap::new();
map.extend(new_globals_2015.clone());
map.extend(NEW_GLOBALS_2017.clone());
map
};
let new_globals_2015_2017_2020 = {
let mut map = new_globals_2015_2017.clone();
map.extend(NEW_GLOBALS_2020.clone());
map
};
let new_globals_2015_2017_2020_2021 = {
let mut map = new_globals_2015_2017_2020.clone();
map.extend(NEW_GLOBALS_2021.clone());
map
};
let envs_preset: Vec<Env> = [
// Language
("builtin", &globals["builtin"]), // oxc uses builtin instead of es5 of ESLint
("es6", &new_globals_2015),
("es2015", &new_globals_2015),
("es2016", &new_globals_2015),
("es2017", &new_globals_2015_2017),
("es2018", &new_globals_2015_2017),
("es2019", &new_globals_2015_2017),
("es2020", &new_globals_2015_2017_2020),
("es2021", &new_globals_2015_2017_2020_2021),
("es2022", &new_globals_2015_2017_2020_2021),
("es2023", &new_globals_2015_2017_2020_2021),
("es2024", &new_globals_2015_2017_2020_2021),
// Platforms
("browser", &globals["browser"]),
("node", &globals["node"]),
("shared-node-browser", &globals["shared-node-browser"]),
("worker", &globals["worker"]),
("serviceworker", &globals["serviceworker"]),
// Frameworks
("commonjs", &globals["commonjs"]),
("amd", &globals["amd"]),
("mocha", &globals["mocha"]),
("jasmine", &globals["jasmine"]),
("jest", &globals["jest"]),
("phantomjs", &globals["phantomjs"]),
("jquery", &globals["jquery"]),
("qunit", &globals["qunit"]),
("prototypejs", &globals["prototypejs"]),
("shelljs", &globals["shelljs"]),
("meteor", &globals["meteor"]),
("mongo", &globals["mongo"]),
("protractor", &globals["protractor"]),
("applescript", &globals["applescript"]),
("nashorn", &globals["nashorn"]),
("atomtest", &globals["atomtest"]),
("embertest", &globals["embertest"]),
("webextensions", &globals["webextensions"]),
("greasemonkey", &globals["greasemonkey"]),
]
.iter()
.map(|(name, vars)| Env { name, vars: to_env_vars(vars) })
.collect();
let context = Context::new(envs_preset);
let template = template::Template::with_context(&context);
if let Err(err) = template.render() {
eprintln!("failed to render environments template: {err}");
}
}
fn to_env_vars(env_var_map: &HashMap<String, bool>) -> Vec<EnvVar> {
let mut result: Vec<EnvVar> = vec![];
for (key, value) in env_var_map {
result.push(EnvVar { name: key, writeable: *value });
}
result.sort_by(|a, b| a.name.cmp(b.name));
result
}

View file

@ -0,0 +1,38 @@
use std::{
fs::File,
io::{Error, Write},
path::Path,
};
use handlebars::Handlebars;
use crate::Context;
const ENV_TEMPLATE: &str = include_str!("../template.hbs");
pub struct Template<'a> {
context: &'a Context<'a>,
registry: Handlebars<'a>,
}
impl<'a> Template<'a> {
pub fn with_context(context: &'a Context) -> Self {
let mut registry = handlebars::Handlebars::new();
registry.register_escape_fn(handlebars::no_escape);
Self { context, registry }
}
pub fn render(&self) -> Result<(), Error> {
let rendered = self
.registry
.render_template(ENV_TEMPLATE, &handlebars::to_json(self.context))
.unwrap();
let out_path = Path::new("crates/oxc_linter/src/javascript_globals.rs");
File::create(out_path)?.write_all(rendered.as_bytes())?;
println!("Saved env file to {out_path:?}");
Ok(())
}
}

View file

@ -0,0 +1,11 @@
use phf::{phf_map, Map};
pub static ENVIRONMENTS: Map<&'static str, Map<&'static str, bool>> = phf_map! {
{{#each envs }}
"{{name}}" => phf_map! {
{{#each vars}}
"{{name}}" => {{writeable}},
{{/each}}
},
{{/each}}
};