mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(linter): check module specifiers and string values that contain lone surrogates (#26717)
Lone surrogates disabled several lint checks and could count as allow-list matches. Module specifiers, names, whitespace and duplicate keys are now checked on the `JSStr` value. Fixes that need UTF-8 text keep the diagnostic and skip the fix. User-configured regexes and globs only match UTF-8 text, so a value with a lone surrogate matches none of them. Allow-lists in `new-cap`, `no-unassigned-import`, `no-require-imports`, `no-large-snapshots` and `prop-name-casing` don't exempt it, `no-restricted-imports` and `valid-title` don't report it, and `valid-title`'s `mustMatch` does. `img-redundant-alt` searches the value's bytes directly. `import/extensions` (for `require()`) and `relative-url-style` still skip such values, as do static imports, because module records omit them. `no-require-imports` now matches `allow` against a template's cooked value, and only when the template has no expressions, like typescript-eslint. The TypeScript `rewriteImportExtensions` option also rewrites specifiers that contain a lone surrogate. Based on #26716. Part of #26242. Implemented with AI assistance.
This commit is contained in:
1 parent
e236de026a
commit
7c47246c71
83 files changed
+1907
-466
No files matched your search
@@ -472,9 +472,9 @@ fn string_literal_key_name<'a>(key: &'a PropertyKey<'a>) -> Option<&'a str> {
|
||||
}
|
||||
}
|
||||
|
||||
fn string_literal_argument<'a>(argument: &'a Argument<'a>) -> Option<&'a str> {
|
||||
fn string_literal_argument<'a>(argument: &'a Argument<'a>) -> Option<JSStr<'a>> {
|
||||
match argument.as_expression()?.without_parentheses() {
|
||||
Expression::StringLiteral(lit) => lit.value.as_str(),
|
||||
Expression::StringLiteral(lit) => Some(lit.value),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -841,6 +841,10 @@ fn test() {
|
||||
"(Object?.defineProperties)(foo, { bar: { value: function bar() {} } })",
|
||||
Some(serde_json::json!(["never", { "considerPropertyDescriptor": true }])),
|
||||
), // { "ecmaVersion": 2020 },
|
||||
(
|
||||
r#"Object.defineProperty(o, "\uD800", { value: function foo() {} })"#,
|
||||
Some(serde_json::json!(["always", { "considerPropertyDescriptor": true }])),
|
||||
),
|
||||
("class C { x = function y() {}; }", Some(serde_json::json!(["always"]))), // { "ecmaVersion": 2022 },
|
||||
("class C { x = function x() {}; }", Some(serde_json::json!(["never"]))), // { "ecmaVersion": 2022 },
|
||||
("class C { 'x' = function y() {}; }", Some(serde_json::json!(["always"]))), // { "ecmaVersion": 2022 },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use lazy_regex::Regex;
|
||||
use oxc_allocator::Allocator;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
@@ -9,7 +10,7 @@ use oxc_ast::{
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::Span;
|
||||
use oxc_str::CompactStr;
|
||||
use oxc_str::{CompactStr, JSChar, JSStr, JSStrBuilder};
|
||||
|
||||
use crate::{
|
||||
AstNode,
|
||||
@@ -435,7 +436,7 @@ impl Rule for NewCap {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(name) = &extract_name_deep_from_expression(callee) else {
|
||||
let Some(name) = extract_name_deep_from_expression(callee, ctx.allocator()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -444,7 +445,7 @@ impl Rule for NewCap {
|
||||
name,
|
||||
self.new_is_cap_exceptions.iter().map(CompactStr::as_str),
|
||||
self.new_is_cap_exception_pattern.as_ref(),
|
||||
) || (!self.properties && short_name != name.as_str());
|
||||
) || (!self.properties && short_name != name);
|
||||
|
||||
if !allowed {
|
||||
ctx.diagnostic(new_cap_diagnostic(short_name_span, capitalization));
|
||||
@@ -463,7 +464,7 @@ impl Rule for NewCap {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(name) = &extract_name_deep_from_expression(callee) else {
|
||||
let Some(name) = extract_name_deep_from_expression(callee, ctx.allocator()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -472,7 +473,7 @@ impl Rule for NewCap {
|
||||
name,
|
||||
self.cap_is_new_exceptions.iter().map(CompactStr::as_str).chain(CAPS_ALLOWED),
|
||||
self.cap_is_new_exception_pattern.as_ref(),
|
||||
) || (!self.properties && short_name != name.as_str());
|
||||
) || (!self.properties && short_name != name);
|
||||
|
||||
if !allowed {
|
||||
ctx.diagnostic(new_cap_diagnostic(short_name_span, capitalization));
|
||||
@@ -483,100 +484,63 @@ impl Rule for NewCap {
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_name_deep_from_expression(expression: &Expression) -> Option<CompactStr> {
|
||||
fn extract_name_deep_from_expression<'a>(
|
||||
expression: &Expression<'a>,
|
||||
allocator: &'a Allocator,
|
||||
) -> Option<JSStr<'a>> {
|
||||
if let Some(identifier) = expression.get_identifier_reference() {
|
||||
return Some(identifier.name.into());
|
||||
}
|
||||
|
||||
match expression.without_parentheses() {
|
||||
Expression::StaticMemberExpression(expression) => {
|
||||
let prop_name = expression.property.name.into_compact_str();
|
||||
let obj_name =
|
||||
extract_name_deep_from_expression(expression.object.without_parentheses());
|
||||
let mut name = JSStrBuilder::new_in(&allocator);
|
||||
append_name_deep(expression, &mut name)?;
|
||||
Some(name.into_js_str())
|
||||
}
|
||||
|
||||
if let Some(obj_name) = obj_name {
|
||||
let new_name = format!("{obj_name}.{prop_name}");
|
||||
return Some(CompactStr::new(&new_name));
|
||||
}
|
||||
|
||||
Some(prop_name)
|
||||
}
|
||||
Expression::ComputedMemberExpression(expression) => {
|
||||
let (prop_name, _) = get_computed_member_name(expression)?;
|
||||
let obj_name =
|
||||
extract_name_deep_from_expression(expression.object.without_parentheses());
|
||||
|
||||
if let Some(obj_name) = obj_name {
|
||||
let new_name = format!("{obj_name}.{prop_name}");
|
||||
return Some(CompactStr::new(&new_name));
|
||||
}
|
||||
|
||||
Some(prop_name.into())
|
||||
}
|
||||
Expression::ChainExpression(chain) => match &chain.expression {
|
||||
ChainElement::CallExpression(call) => extract_name_deep_from_expression(&call.callee),
|
||||
fn append_name_deep(expression: &Expression<'_>, name: &mut JSStrBuilder<'_>) -> Option<()> {
|
||||
if let Expression::ChainExpression(chain) = expression.without_parentheses() {
|
||||
match &chain.expression {
|
||||
ChainElement::CallExpression(call) => return append_name_deep(&call.callee, name),
|
||||
ChainElement::TSNonNullExpression(non_null) => {
|
||||
extract_name_deep_from_expression(&non_null.expression)
|
||||
return append_name_deep(&non_null.expression, name);
|
||||
}
|
||||
ChainElement::StaticMemberExpression(expression) => {
|
||||
let prop_name = expression.property.name.into_compact_str();
|
||||
let obj_name =
|
||||
extract_name_deep_from_expression(expression.object.without_parentheses());
|
||||
|
||||
if let Some(obj_name) = obj_name {
|
||||
let new_name = format!("{obj_name}.{prop_name}");
|
||||
return Some(CompactStr::new(&new_name));
|
||||
}
|
||||
|
||||
Some(prop_name)
|
||||
}
|
||||
ChainElement::ComputedMemberExpression(expression) => {
|
||||
let (prop_name, _) = get_computed_member_name(expression)?;
|
||||
let obj_name =
|
||||
extract_name_deep_from_expression(expression.object.without_parentheses());
|
||||
|
||||
if let Some(obj_name) = obj_name {
|
||||
let new_name = format!("{obj_name}.{prop_name}");
|
||||
return Some(CompactStr::new(&new_name));
|
||||
}
|
||||
|
||||
Some(prop_name.into())
|
||||
}
|
||||
ChainElement::PrivateFieldExpression(_) => None,
|
||||
},
|
||||
_ => None,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let (prop_name, _) = extract_name_from_expression(expression)?;
|
||||
if let Some(member) = expression.get_member_expr()
|
||||
&& append_name_deep(member.object(), name).is_some()
|
||||
{
|
||||
name.push('.');
|
||||
}
|
||||
name.push_js_str(prop_name);
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn get_computed_member_name<'a>(
|
||||
computed_member: &ComputedMemberExpression<'a>,
|
||||
) -> Option<(&'a str, Span)> {
|
||||
) -> Option<(JSStr<'a>, Span)> {
|
||||
let expression = computed_member.expression.without_parentheses();
|
||||
|
||||
match &expression {
|
||||
Expression::StringLiteral(lit) if !lit.value.is_empty() => {
|
||||
Some((lit.value.as_str()?, lit.span))
|
||||
Expression::StringLiteral(lit) if !lit.value.is_empty() => Some((lit.value, lit.span)),
|
||||
Expression::TemplateLiteral(lit) => {
|
||||
lit.single_quasi().filter(|value| !value.is_empty()).map(|value| (value, lit.span))
|
||||
}
|
||||
Expression::TemplateLiteral(lit)
|
||||
if lit.expressions.is_empty()
|
||||
&& lit.quasis.len() == 1
|
||||
&& !lit.quasis[0].value.raw.is_empty() =>
|
||||
{
|
||||
Some((lit.quasis[0].value.raw.as_str(), lit.span))
|
||||
}
|
||||
Expression::RegExpLiteral(lit) => lit.raw.as_ref().map(|x| (x.as_str(), lit.span)),
|
||||
Expression::RegExpLiteral(lit) => lit.raw.map(|raw| (JSStr::from(raw), lit.span)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_name_from_expression<'a>(expression: &Expression<'a>) -> Option<(&'a str, Span)> {
|
||||
fn extract_name_from_expression<'a>(expression: &Expression<'a>) -> Option<(JSStr<'a>, Span)> {
|
||||
if let Some(identifier) = expression.get_identifier_reference() {
|
||||
return Some((identifier.name.as_str(), identifier.span));
|
||||
return Some((JSStr::from(identifier.name), identifier.span));
|
||||
}
|
||||
|
||||
match expression.without_parentheses() {
|
||||
Expression::StaticMemberExpression(expression) => {
|
||||
Some((expression.property.name.as_str(), expression.property.span))
|
||||
Some((JSStr::from(expression.property.name), expression.property.span))
|
||||
}
|
||||
Expression::ComputedMemberExpression(expression) => get_computed_member_name(expression),
|
||||
Expression::ChainExpression(chain) => match &chain.expression {
|
||||
@@ -585,7 +549,7 @@ fn extract_name_from_expression<'a>(expression: &Expression<'a>) -> Option<(&'a
|
||||
extract_name_from_expression(&non_null.expression)
|
||||
}
|
||||
ChainElement::StaticMemberExpression(expression) => {
|
||||
Some((expression.property.name.as_str(), expression.property.span))
|
||||
Some((JSStr::from(expression.property.name), expression.property.span))
|
||||
}
|
||||
ChainElement::ComputedMemberExpression(expression) => {
|
||||
get_computed_member_name(expression)
|
||||
@@ -597,8 +561,8 @@ fn extract_name_from_expression<'a>(expression: &Expression<'a>) -> Option<(&'a
|
||||
}
|
||||
|
||||
fn is_cap_allowed_expression<'a, I>(
|
||||
short_name: &str,
|
||||
name: &CompactStr,
|
||||
short_name: JSStr<'_>,
|
||||
name: JSStr<'_>,
|
||||
exceptions: I,
|
||||
patterns: Option<&Regex>,
|
||||
) -> bool
|
||||
@@ -606,7 +570,7 @@ where
|
||||
I: Iterator<Item = &'a str>,
|
||||
{
|
||||
for exception in exceptions {
|
||||
if exception == name.as_str() || exception == short_name {
|
||||
if name == exception || short_name == exception {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -616,7 +580,8 @@ where
|
||||
}
|
||||
|
||||
if let Some(pattern) = &patterns {
|
||||
return pattern.find(name).is_some();
|
||||
// A name with a lone surrogate matches no exception pattern.
|
||||
return name.as_str().is_some_and(|name| pattern.is_match(name));
|
||||
}
|
||||
|
||||
false
|
||||
@@ -629,8 +594,12 @@ enum GetCapResult {
|
||||
NonAlpha,
|
||||
}
|
||||
|
||||
fn get_cap(string: &str) -> GetCapResult {
|
||||
let first_char = string.chars().next().unwrap();
|
||||
fn get_cap(name: JSStr) -> GetCapResult {
|
||||
// A lone surrogate has no case (`toUpperCase` leaves it unchanged),
|
||||
// so ESLint classifies such a name as non-alphabetic.
|
||||
let Some(first_char) = name.chars().next().and_then(JSChar::to_char) else {
|
||||
return GetCapResult::NonAlpha;
|
||||
};
|
||||
|
||||
if !first_char.is_alphabetic() {
|
||||
return GetCapResult::NonAlpha;
|
||||
@@ -648,6 +617,11 @@ fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
// Lone surrogates are part of the value.
|
||||
// A search or prefix check must still see the rest.
|
||||
(r#"var x = new o["\uD800a"]();"#, None),
|
||||
(r#"var x = new o["a\uD800"]();"#, Some(serde_json::json!([{ "properties": false }]))),
|
||||
(r#"var x = new o["A\uD83D\uDE00"]();"#, None),
|
||||
("var x = new Constructor();", None),
|
||||
("var x = new a.b.Constructor();", None),
|
||||
("var x = new a.b['Constructor']();", None),
|
||||
@@ -733,11 +707,43 @@ fn test() {
|
||||
"capIsNewExceptions": ["services.registry.CreateDocumentForCurrentSession"]
|
||||
}])),
|
||||
),
|
||||
// Template literal property names use their raw text.
|
||||
(r"services.registry[`\u0043reateDocumentForCurrentSession`]();", None),
|
||||
(
|
||||
r"services.registry[`\u0043reateDocumentForCurrentSession`]();",
|
||||
Some(serde_json::json!([{
|
||||
"capIsNewExceptions": ["services.registry.CreateDocumentForCurrentSession"]
|
||||
}])),
|
||||
),
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
(
|
||||
r"new o['a\uD800']();",
|
||||
Some(serde_json::json!([{ "newIsCapExceptions": [r#"o."a\ud800""#] }])),
|
||||
),
|
||||
(
|
||||
r"o['A\uDC00']();",
|
||||
Some(serde_json::json!([{ "capIsNewExceptions": [r#"o."A\udc00""#] }])),
|
||||
),
|
||||
(
|
||||
r"new o['a\uD800']();",
|
||||
Some(serde_json::json!([{ "newIsCapExceptionPattern": "^other" }])),
|
||||
),
|
||||
// A name with a lone surrogate matches no exception pattern.
|
||||
(
|
||||
r"new o['a\uD800']();",
|
||||
Some(serde_json::json!([{ "newIsCapExceptionPattern": "^o\\.a" }])),
|
||||
),
|
||||
(r"o['A\uDC00']();", Some(serde_json::json!([{ "capIsNewExceptionPattern": "^o\\.A" }]))),
|
||||
(
|
||||
r"(o?.['A\uDC00'])();",
|
||||
Some(serde_json::json!([{ "capIsNewExceptionPattern": "^o\\.A" }])),
|
||||
),
|
||||
(r"services.registry[`\u0043reateDocumentForCurrentSession`]();", None),
|
||||
(r"new o[`a\uD800`]();", None),
|
||||
// Lone surrogates are part of the value.
|
||||
// A search or prefix check must still see the rest.
|
||||
(r#"var x = new o["a\uD800"]();"#, None),
|
||||
(r#"var x = o["A\uDC00"]();"#, None),
|
||||
("var x = new c();", None),
|
||||
("var x = new φ;", None),
|
||||
("var x = new a.b.c;", None),
|
||||
|
||||
@@ -2033,6 +2033,13 @@ fn test() {
|
||||
),
|
||||
// dynamic `import()` with a string-literal source
|
||||
("import('os');", Some(serde_json::json!(["fs"]))),
|
||||
// A source with a lone surrogate matches no configured name or pattern.
|
||||
(r"import('a\uD800');", Some(serde_json::json!(["a\u{FFFD}"]))),
|
||||
(r"import('a\uD800');", Some(serde_json::json!([{ "patterns": [{ "regex": "^a.$" }] }]))),
|
||||
(
|
||||
r"import('a\uD800/b');",
|
||||
Some(serde_json::json!([{ "patterns": [{ "group": ["a*/b"] }] }])),
|
||||
),
|
||||
("import('foo/bar');", Some(serde_json::json!([{ "patterns": ["foo/baz"] }]))),
|
||||
// non-literal dynamic import sources are ignored
|
||||
("import(foo);", Some(serde_json::json!(["foo"]))),
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_ast::{
|
||||
AstKind,
|
||||
ast::{Argument, Expression},
|
||||
builder::AstBuilder,
|
||||
};
|
||||
use oxc_codegen::Codegen;
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_regular_expression::{
|
||||
LiteralParser, Options,
|
||||
ConstructorParser, LiteralParser, Options,
|
||||
ast::Pattern,
|
||||
visit::{RegExpAstKind, Visit},
|
||||
};
|
||||
use oxc_span::{GetSpan, Span};
|
||||
use oxc_str::JSStr;
|
||||
|
||||
use crate::{
|
||||
AstNode,
|
||||
@@ -110,29 +115,50 @@ fn check_static_arguments(arg0: Option<&Argument>, arg1: Option<&Argument>, ctx:
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(pattern_text) = static_string_value(pattern_expr) else {
|
||||
let Some(pattern_value) = static_string_value(pattern_expr, ctx.allocator()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let flags_text = arg1
|
||||
let flags_value = arg1
|
||||
.and_then(Argument::as_expression)
|
||||
.map(Expression::get_inner_expression)
|
||||
.and_then(static_string_value);
|
||||
|
||||
let allocator = Allocator::default();
|
||||
let Ok(pattern) = LiteralParser::new(
|
||||
&allocator,
|
||||
&pattern_text,
|
||||
flags_text.as_deref(),
|
||||
Options { pattern_span_offset: pattern_expr.span().start, flags_span_offset: 0 },
|
||||
)
|
||||
.parse() else {
|
||||
return;
|
||||
.and_then(|expr| static_string_value(expr, ctx.allocator()));
|
||||
let flags_text = match flags_value {
|
||||
Some(value) => {
|
||||
let Some(text) = value.as_str() else { return };
|
||||
Some(text)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let allocator = Allocator::default();
|
||||
let options = Options { pattern_span_offset: pattern_expr.span().start, flags_span_offset: 0 };
|
||||
let pattern_text = pattern_value.as_str().map_or_else(
|
||||
|| Cow::Owned(string_literal_source(pattern_value, &allocator)),
|
||||
Cow::Borrowed,
|
||||
);
|
||||
let flags_source = pattern_value
|
||||
.has_lone_surrogate()
|
||||
.then(|| flags_text.map(|text| string_literal_source(text.into(), &allocator)))
|
||||
.flatten();
|
||||
let parsed = if pattern_value.has_lone_surrogate() {
|
||||
// Parse the generated string literal to retain the original UTF-16 values.
|
||||
ConstructorParser::new(&allocator, &pattern_text, flags_source.as_deref(), options).parse()
|
||||
} else {
|
||||
LiteralParser::new(&allocator, &pattern_text, flags_text, options).parse()
|
||||
};
|
||||
let Ok(pattern) = parsed else { return };
|
||||
|
||||
check_pattern(&pattern, ctx, Some(pattern_expr.span()));
|
||||
}
|
||||
|
||||
fn string_literal_source(value: JSStr<'_>, allocator: &Allocator) -> String {
|
||||
let expression =
|
||||
Expression::new_string_literal(Span::default(), value, None, &AstBuilder::new(allocator));
|
||||
let mut codegen = Codegen::new();
|
||||
codegen.print_expression(&expression);
|
||||
codegen.into_source_text()
|
||||
}
|
||||
|
||||
fn is_directly_supported_regex_argument(expr: &Expression<'_>) -> bool {
|
||||
match expr {
|
||||
Expression::RegExpLiteral(_) | Expression::StringLiteral(_) => true,
|
||||
@@ -161,6 +187,9 @@ fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
r#"new RegExp("(a)[" + "\uD801" + "-" + "\uD800" + "]");"#,
|
||||
r#"new RegExp("(a)[" + "\uD801" + "-" + "\uD800" + "]", "u");"#,
|
||||
r#"new RegExp("(a)" + "", "u\uD800");"#,
|
||||
"/normal_regex/",
|
||||
"/(?:[0-9]{4})/",
|
||||
"/(?<year>[0-9]{4})/",
|
||||
@@ -193,6 +222,8 @@ fn test() {
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
r#"new RegExp("(a)[" + "\uD800" + "-" + "\uE000" + "]");"#,
|
||||
r#"new RegExp("(a)[" + "\uD800" + "-" + "\uE000" + "]", "u");"#,
|
||||
"/([0-9]{4})/",
|
||||
"new RegExp('([0-9]{4})')",
|
||||
"RegExp('([0-9]{4})')",
|
||||
@@ -210,6 +241,7 @@ fn test() {
|
||||
"new RegExp('a(bc)d' + 'e')",
|
||||
r#"new RegExp("foo" + "(a)" + "(b)");"#,
|
||||
r#"new RegExp("foo" + "(?:a)" + "(b)");"#,
|
||||
r#"new RegExp("(a)" + "\uD800");"#,
|
||||
"RegExp('(a)'+'')",
|
||||
"RegExp( '' + '(ab)')",
|
||||
"new RegExp(`(ab)${''}`)",
|
||||
|
||||
@@ -9,6 +9,7 @@ use oxc_ast::{
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::{GetSpan, Span};
|
||||
use oxc_str::{JSChar, JSStr};
|
||||
use oxc_syntax::operator::{AssignmentOperator, BinaryOperator};
|
||||
|
||||
use crate::{
|
||||
@@ -236,8 +237,14 @@ fn extract_regex_flags<'a>(
|
||||
resolve_flags(flags_arg, ctx, true, 0)
|
||||
}
|
||||
|
||||
fn parse_flags(flags_text: &str) -> RegExpFlags {
|
||||
flags_text.chars().filter_map(|ch| RegExpFlags::try_from(ch).ok()).collect()
|
||||
fn parse_flags(flags: JSStr) -> RegExpFlags {
|
||||
// A lone surrogate is never a flag.
|
||||
// The other characters still count, so a `u` beside one is still seen.
|
||||
flags
|
||||
.chars()
|
||||
.filter_map(JSChar::to_char)
|
||||
.filter_map(|ch| RegExpFlags::try_from(ch).ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
const MAX_RESOLVE_DEPTH: usize = 8;
|
||||
@@ -259,7 +266,7 @@ fn resolve_flags<'a>(
|
||||
}
|
||||
|
||||
match expr {
|
||||
Expression::StringLiteral(lit) => Some(parse_flags(lit.value.as_str()?)),
|
||||
Expression::StringLiteral(lit) => Some(parse_flags(lit.value)),
|
||||
Expression::TemplateLiteral(template) => resolve_template_flags(template, ctx, next_depth),
|
||||
Expression::BooleanLiteral(lit) => {
|
||||
Some(if lit.value { RegExpFlags::U } else { RegExpFlags::empty() })
|
||||
@@ -274,8 +281,12 @@ fn resolve_flags<'a>(
|
||||
Expression::ComputedMemberExpression(member) => {
|
||||
let object = resolve_static_string(&member.object, ctx)?;
|
||||
let index = resolve_static_index(&member.expression)?;
|
||||
let ch = object.chars().nth(index)?;
|
||||
Some(RegExpFlags::try_from(ch).unwrap_or_else(|_| RegExpFlags::empty()))
|
||||
let unit = object.encode_utf16().nth(index)?;
|
||||
Some(
|
||||
char::from_u32(u32::from(unit))
|
||||
.and_then(|ch| RegExpFlags::try_from(ch).ok())
|
||||
.unwrap_or_else(RegExpFlags::empty),
|
||||
)
|
||||
}
|
||||
Expression::SequenceExpression(sequence) => {
|
||||
resolve_flags(sequence.expressions.last()?, ctx, true, next_depth)
|
||||
@@ -297,22 +308,22 @@ fn resolve_template_flags<'a>(
|
||||
let mut flags = RegExpFlags::empty();
|
||||
|
||||
for (index, expression) in template.expressions.iter().enumerate() {
|
||||
flags |= parse_flags(template.quasis.get(index)?.value.cooked?.as_str()?);
|
||||
flags |= parse_flags(template.quasis.get(index)?.value.cooked?);
|
||||
flags |= resolve_flags(expression, ctx, true, depth)?;
|
||||
}
|
||||
|
||||
flags |= parse_flags(template.quasis.last()?.value.cooked?.as_str()?);
|
||||
flags |= parse_flags(template.quasis.last()?.value.cooked?);
|
||||
Some(flags)
|
||||
}
|
||||
|
||||
fn resolve_static_string<'a>(expr: &'a Expression<'a>, ctx: &LintContext<'a>) -> Option<&'a str> {
|
||||
fn resolve_static_string<'a>(expr: &'a Expression<'a>, ctx: &LintContext<'a>) -> Option<JSStr<'a>> {
|
||||
match expr.get_inner_expression() {
|
||||
Expression::StringLiteral(lit) => lit.value.as_str(),
|
||||
Expression::TemplateLiteral(template) => template.single_quasi()?.as_str(),
|
||||
Expression::StringLiteral(lit) => Some(lit.value),
|
||||
Expression::TemplateLiteral(template) => template.single_quasi(),
|
||||
Expression::Identifier(ident) => {
|
||||
match resolve_const_initializer(ident, ctx)?.get_inner_expression() {
|
||||
Expression::StringLiteral(lit) => lit.value.as_str(),
|
||||
Expression::TemplateLiteral(template) => template.single_quasi()?.as_str(),
|
||||
Expression::StringLiteral(lit) => Some(lit.value),
|
||||
Expression::TemplateLiteral(template) => template.single_quasi(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -356,6 +367,12 @@ fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
(r#"new RegExp("a", "😀u\uD800"[2]);"#, None),
|
||||
(r#"new RegExp("a", "😀u"[2]);"#, None),
|
||||
// Lone surrogates are part of the value.
|
||||
// A search or prefix check must still see the rest.
|
||||
(r#"new RegExp("foo", "u\uD800")"#, None),
|
||||
(r#"new RegExp("foo", `\uDC00u`)"#, None),
|
||||
("/foo/u", None),
|
||||
("/foo/gimuy", None),
|
||||
("RegExp('', 'u')", None),
|
||||
@@ -399,6 +416,10 @@ fn test() {
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
// Lone surrogates are part of the value.
|
||||
// A search or prefix check must still see the rest.
|
||||
(r#"new RegExp("foo", "\uD800")"#, None),
|
||||
(r#"new RegExp("foo", `g\uDC00`)"#, None),
|
||||
(r"/\a/", None),
|
||||
("/foo/", None),
|
||||
("/foo/gimy", None),
|
||||
|
||||
@@ -9,6 +9,7 @@ use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_ecmascript::{ToBigInt, WithoutGlobalReferenceInformation};
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::{GetSpan, Span};
|
||||
use oxc_str::JSStr;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
@@ -404,7 +405,7 @@ fn is_range(expr: &LogicalExpression, ctx: &LintContext) -> bool {
|
||||
if let (Some(left_left), Some(right_right)) =
|
||||
(get_string_literal(left_left), get_string_literal(right_right))
|
||||
{
|
||||
return left_left <= right_right;
|
||||
return is_string_le(left_left, right_right);
|
||||
}
|
||||
|
||||
if let (Some(left_left), Some(right_right)) =
|
||||
@@ -438,7 +439,7 @@ fn is_range(expr: &LogicalExpression, ctx: &LintContext) -> bool {
|
||||
if let (Some(left_right), Some(right_left)) =
|
||||
(get_string_literal(left_right), get_string_literal(right_left))
|
||||
{
|
||||
return left_right <= right_left;
|
||||
return is_string_le(left_right, right_left);
|
||||
}
|
||||
|
||||
if let (Some(left_right), Some(right_left)) =
|
||||
@@ -468,15 +469,25 @@ fn is_target_literal(expr: &Expression) -> bool {
|
||||
get_string_literal(expr).is_some() || is_number(expr)
|
||||
}
|
||||
|
||||
fn get_string_literal<'a>(expr: &'a Expression) -> Option<&'a str> {
|
||||
/// Compares strings by bytes.
|
||||
/// A string with a lone surrogate has no UTF-8 form, so it is compared by UTF-16 code unit.
|
||||
fn is_string_le(left: JSStr, right: JSStr) -> bool {
|
||||
if left.has_lone_surrogate() || right.has_lone_surrogate() {
|
||||
left.encode_utf16().le(right.encode_utf16())
|
||||
} else {
|
||||
left.as_bytes() <= right.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_string_literal<'a>(expr: &Expression<'a>) -> Option<JSStr<'a>> {
|
||||
match expr {
|
||||
Expression::StringLiteral(string) => string.value.as_str(),
|
||||
Expression::StringLiteral(string) => Some(string.value),
|
||||
Expression::TemplateLiteral(template) => {
|
||||
if template.quasis.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
template.quasis.first().map(|e| e.value.raw.as_str())
|
||||
template.quasis.first().map(|e| JSStr::from(e.value.raw))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
@@ -523,6 +534,19 @@ fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
// A lone surrogate orders by its UTF-16 code unit.
|
||||
(
|
||||
r#"if ("\uD800" <= x && x <= "\uDBFF") {}"#,
|
||||
Some(serde_json::json!(["never", { "exceptRange": true }])),
|
||||
),
|
||||
(
|
||||
r#"if ("\uDC00" <= x && x <= "\uDFFF") {}"#,
|
||||
Some(serde_json::json!(["never", { "exceptRange": true }])),
|
||||
),
|
||||
(
|
||||
r#"if ("a\uD800" <= x && x <= "b") {}"#,
|
||||
Some(serde_json::json!(["never", { "exceptRange": true }])),
|
||||
),
|
||||
(r#"if (value === "red") {}"#, Some(serde_json::json!(["never"]))),
|
||||
("if (value === value) {}", Some(serde_json::json!(["never"]))),
|
||||
("if (value != 5) {}", Some(serde_json::json!(["never"]))),
|
||||
@@ -699,6 +723,11 @@ fn test() {
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
// A lone surrogate orders by its UTF-16 code unit.
|
||||
(
|
||||
r#"if ("\uDBFF" <= x && x <= "\uD800") {}"#,
|
||||
Some(serde_json::json!(["never", { "exceptRange": true }])),
|
||||
),
|
||||
(
|
||||
"if (x <= 'foo' || 'bar' < x) {}",
|
||||
Some(serde_json::json!(["always", { "exceptRange": true }])),
|
||||
|
||||
@@ -10,6 +10,7 @@ use oxc_codegen::{Context, Gen};
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::{FileExtension, GetSpan, SPAN, Span};
|
||||
use oxc_str::JSStr;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
@@ -314,8 +315,9 @@ fn gen_type_import_declaration<'c, 'a: 'c>(
|
||||
}
|
||||
|
||||
fn is_declaration_file_import(import_decl: &ImportDeclaration) -> bool {
|
||||
let Some(source) = import_decl.source.value.as_str() else {
|
||||
return false;
|
||||
let value = import_decl.source.value;
|
||||
let Some(source) = value.as_str() else {
|
||||
return is_declaration_file_js_path(value);
|
||||
};
|
||||
// Relatively fast check to avoid unnecessary Path and extension parsing
|
||||
// if it doesn't even look like a declaration file import
|
||||
@@ -333,12 +335,29 @@ fn is_declaration_file_import(import_decl: &ImportDeclaration) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// `is_declaration_file_import` for a source with a lone surrogate, which `std::path` cannot hold.
|
||||
/// The extension is ASCII, so suffix checks decide the same way
|
||||
/// `FileExtension::is_ts_declaration` does.
|
||||
fn is_declaration_file_js_path(source: JSStr) -> bool {
|
||||
if source.ends_with(".ts") {
|
||||
source.rfind(".d.").is_some_and(|i| i != 0)
|
||||
} else if source.ends_with(".mts") || source.ends_with(".cts") {
|
||||
source.len() > 6 && (source.ends_with(".d.mts") || source.ends_with(".d.cts"))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use crate::tester::Tester;
|
||||
use serde_json::json;
|
||||
|
||||
let pass = vec![
|
||||
// Lone surrogates are part of the value.
|
||||
// A search or prefix check must still see the rest.
|
||||
(r"import type { Foo } from './\uD800.d.ts';", Some(json!(["prefer-inline"]))),
|
||||
(r"import type { Foo } from './\uDC00.d.mts';", Some(json!(["prefer-inline"]))),
|
||||
("import Foo from 'Foo'", None),
|
||||
("import type Foo from 'Foo'", None),
|
||||
("import { Foo } from 'Foo';", None),
|
||||
@@ -415,6 +434,9 @@ fn test() {
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
// Lone surrogates are part of the value.
|
||||
// A search or prefix check must still see the rest.
|
||||
(r"import type { Foo } from './\uD800.ts';", Some(json!(["prefer-inline"]))),
|
||||
("import { type Foo, type Bar } from 'Foo'", None),
|
||||
("import type { Foo } from 'Foo'", Some(json!(["prefer-inline"]))),
|
||||
("import { type Foo as Bar } from 'Foo';", None),
|
||||
|
||||
@@ -878,6 +878,8 @@ fn test() {
|
||||
use serde_json::json;
|
||||
|
||||
let pass = vec![
|
||||
// A `require()` specifier with a lone surrogate is not checked.
|
||||
(r#"const foo = require("./foo\uD800");"#, Some(json!(["always"]))),
|
||||
// Default config: no extension requirements, unconfigured extensions are ignored
|
||||
(r#"import a from "@/a""#, None),
|
||||
(r#"import a from "a""#, None),
|
||||
|
||||
@@ -2,6 +2,7 @@ use oxc_ast::ast::{Statement, TSModuleReference};
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::Span;
|
||||
use oxc_str::JSStr;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -84,7 +85,7 @@ declare_oxc_lint!(
|
||||
short_description = "Forbids any non-import statements before imports except directives.",
|
||||
);
|
||||
|
||||
fn is_relative_path(path: &str) -> bool {
|
||||
fn is_relative_path(path: JSStr) -> bool {
|
||||
// A path is considered relative if it starts with "/", "./", or "../"
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#module_specifier_resolution
|
||||
path.starts_with("./") || path.starts_with("../") || path.starts_with('/')
|
||||
@@ -107,7 +108,7 @@ impl Rule for First {
|
||||
Statement::TSImportEqualsDeclaration(decl) => match &decl.module_reference {
|
||||
TSModuleReference::ExternalModuleReference(mod_ref) => {
|
||||
if matches!(self.0, AbsoluteFirst::AbsoluteFirst) {
|
||||
if mod_ref.expression.value.as_str().is_some_and(is_relative_path) {
|
||||
if is_relative_path(mod_ref.expression.value) {
|
||||
any_relative = true;
|
||||
} else if any_relative {
|
||||
ctx.diagnostic(absolute_first_diagnostic(mod_ref.expression.span));
|
||||
@@ -122,7 +123,7 @@ impl Rule for First {
|
||||
},
|
||||
Statement::ImportDeclaration(decl) => {
|
||||
if matches!(self.0, AbsoluteFirst::AbsoluteFirst) {
|
||||
if decl.source.value.as_str().is_some_and(is_relative_path) {
|
||||
if is_relative_path(decl.source.value) {
|
||||
any_relative = true;
|
||||
} else if any_relative {
|
||||
ctx.diagnostic(absolute_first_diagnostic(decl.source.span));
|
||||
@@ -147,6 +148,9 @@ fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
// Lone surrogates are part of the value.
|
||||
// A search or prefix check must still see the rest.
|
||||
(r"import { y } from 'bar'; import { x } from './\uD800'", Some(json!(["absolute-first"]))),
|
||||
(
|
||||
r"import { x } from './foo'; import { y } from './bar';
|
||||
export { x, y }",
|
||||
@@ -186,6 +190,17 @@ fn test() {
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
// Lone surrogates are part of the value.
|
||||
// A search or prefix check must still see the rest.
|
||||
(r"import { x } from './\uD800'; import { y } from 'bar'", Some(json!(["absolute-first"]))),
|
||||
(
|
||||
r"import { x } from '../\uDC00'; import { y } from 'bar'",
|
||||
Some(json!(["absolute-first"])),
|
||||
),
|
||||
(
|
||||
r"import { x } from '/\uD83D\uDE00'; import { y } from 'bar'",
|
||||
Some(json!(["absolute-first"])),
|
||||
),
|
||||
(
|
||||
r"import { x } from './foo';
|
||||
export { x };
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::path::Path;
|
||||
|
||||
use oxc_str::JSStr;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -118,8 +120,7 @@ impl Rule for NoAbsolutePath {
|
||||
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
||||
match node.kind() {
|
||||
AstKind::ImportDeclaration(import_decl)
|
||||
if self.esmodule
|
||||
&& import_decl.source.value.as_str().is_some_and(check_path_is_absolute) =>
|
||||
if self.esmodule && check_path_is_absolute(import_decl.source.value) =>
|
||||
{
|
||||
ctx.diagnostic(no_absolute_path_diagnostic(import_decl.source.span));
|
||||
}
|
||||
@@ -135,17 +136,14 @@ impl Rule for NoAbsolutePath {
|
||||
if count == 1
|
||||
&& func_name == "require"
|
||||
&& self.commonjs
|
||||
&& str_literal
|
||||
.value
|
||||
.as_str()
|
||||
.is_some_and(check_path_is_absolute) =>
|
||||
&& check_path_is_absolute(str_literal.value) =>
|
||||
{
|
||||
ctx.diagnostic(no_absolute_path_diagnostic(str_literal.span));
|
||||
}
|
||||
Argument::ArrayExpression(arr_expr) if count == 2 && self.amd => {
|
||||
for el in &arr_expr.elements {
|
||||
if let Some(el_expr) = el.as_expression()
|
||||
&& matches!(el_expr, Expression::StringLiteral(literal) if literal.value.as_str().is_some_and(check_path_is_absolute))
|
||||
&& matches!(el_expr, Expression::StringLiteral(literal) if check_path_is_absolute(literal.value))
|
||||
{
|
||||
ctx.diagnostic(no_absolute_path_diagnostic(el_expr.span()));
|
||||
}
|
||||
@@ -160,8 +158,14 @@ impl Rule for NoAbsolutePath {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_path_is_absolute(path_str: &str) -> bool {
|
||||
Path::new(path_str).is_absolute()
|
||||
fn check_path_is_absolute(path: JSStr) -> bool {
|
||||
match path.as_str() {
|
||||
Some(path) => Path::new(path).is_absolute(),
|
||||
// `std::path` requires UTF-8.
|
||||
// A path with a lone surrogate is absolute when it starts with `/`,
|
||||
// which is what Node's `path.isAbsolute` checks.
|
||||
None => path.starts_with('/'),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -171,6 +175,10 @@ fn test() {
|
||||
use serde_json::json;
|
||||
|
||||
let pass = vec![
|
||||
// Lone surrogates are part of the value.
|
||||
// A search or prefix check must still see the rest.
|
||||
(r"import _ from './\uD800'", None),
|
||||
(r"import _ from '\uDC00/x'", None),
|
||||
(r"import _ from 'lodash'", None),
|
||||
(r"import _ from '/lodash'", Some(json!([{ "esmodule": false }]))),
|
||||
(r"import _ from './lodash'", None),
|
||||
@@ -216,6 +224,11 @@ fn test() {
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
// Lone surrogates are part of the value.
|
||||
// A search or prefix check must still see the rest.
|
||||
(r"import _ from '/\uD800'", None),
|
||||
(r"var f = require('/\uDC00/x')", Some(json!([{ "commonjs": true }]))),
|
||||
(r"require(['/\uD83D\uDE00'], function(){})", Some(json!([{ "amd": true }]))),
|
||||
(r"import _ from '/lodash'", None),
|
||||
(r"import _ from '/lodash'", Some(json!([{ "esmodule": true }]))),
|
||||
(r"import f from '/foo/path'", None),
|
||||
|
||||
@@ -6,7 +6,7 @@ use oxc_ast::{
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::{GetSpan, Span};
|
||||
use oxc_str::{CompactStr, JSStr};
|
||||
use oxc_str::CompactStr;
|
||||
use rustc_hash::FxHashSet;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -17,7 +17,7 @@ use crate::{
|
||||
rule::{DefaultRuleConfig, Rule},
|
||||
};
|
||||
|
||||
fn no_nodejs_modules_diagnostic(span: Span, module_name: &str) -> OxcDiagnostic {
|
||||
fn no_nodejs_modules_diagnostic(span: Span, module_name: impl std::fmt::Display) -> OxcDiagnostic {
|
||||
OxcDiagnostic::warn(format!("Do not import Node.js builtin module `{module_name}`"))
|
||||
.with_help("Use a browser-compatible alternative or add this module to the `allow` list if Node.js usage is intentional.")
|
||||
.with_label(span)
|
||||
@@ -112,16 +112,25 @@ impl Rule for NoNodejsModules {
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let Some(module_name) = module_name.and_then(JSStr::as_str) else {
|
||||
let Some(module_name) = module_name else {
|
||||
return;
|
||||
};
|
||||
|
||||
if self.allow.contains(module_name) {
|
||||
let Some(name) = module_name.as_str() else {
|
||||
// A name with a lone surrogate matches neither the `allow` list nor a builtin,
|
||||
// but the `node:` scheme still marks a Node.js module.
|
||||
if module_name.starts_with("node:") {
|
||||
ctx.diagnostic(no_nodejs_modules_diagnostic(node.span(), module_name.display()));
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
if self.allow.contains(name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if module_name.starts_with("node:") || is_nodejs_builtin_module(module_name) {
|
||||
ctx.diagnostic(no_nodejs_modules_diagnostic(node.span(), module_name));
|
||||
if name.starts_with("node:") || is_nodejs_builtin_module(name) {
|
||||
ctx.diagnostic(no_nodejs_modules_diagnostic(node.span(), name));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,6 +140,10 @@ fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
// Lone surrogates are part of the value.
|
||||
// A search or prefix check must still see the rest.
|
||||
(r#"import x from "\uD800""#, None),
|
||||
(r#"var x = require("\uDC00")"#, None),
|
||||
// Non Node.js modules
|
||||
(r#"import _ from "lodash""#, None),
|
||||
(r#"import find from "lodash.find""#, None),
|
||||
@@ -184,6 +197,11 @@ fn test() {
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
// Lone surrogates are part of the value.
|
||||
// A search or prefix check must still see the rest.
|
||||
(r#"import x from "node:\uD800""#, None),
|
||||
(r#"var x = require("node:\uDC00")"#, None),
|
||||
(r#"import x from "node:\uD83D\uDE00""#, None),
|
||||
// Node.js builtin modules
|
||||
(r#"import path from "path""#, None),
|
||||
(r#"import fs from "fs""#, None),
|
||||
|
||||
@@ -2,6 +2,7 @@ use oxc_ast::{AstKind, ast::Expression};
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::Span;
|
||||
use oxc_str::JSStr;
|
||||
|
||||
use crate::{AstNode, context::LintContext, rule::Rule};
|
||||
|
||||
@@ -56,26 +57,26 @@ impl Rule for NoRelativeParentImports {
|
||||
match node.kind() {
|
||||
// ESM import declarations
|
||||
AstKind::ImportDeclaration(import_decl)
|
||||
if import_decl.source.value.as_str().is_some_and(is_parent_import) =>
|
||||
if is_parent_import(import_decl.source.value) =>
|
||||
{
|
||||
ctx.diagnostic(no_relative_parent_imports_diagnostic(import_decl.source.span));
|
||||
}
|
||||
// ESM export { } from '...'
|
||||
AstKind::ExportFromDeclaration(export_decl) => {
|
||||
if export_decl.source.value.as_str().is_some_and(is_parent_import) {
|
||||
if is_parent_import(export_decl.source.value) {
|
||||
ctx.diagnostic(no_relative_parent_imports_diagnostic(export_decl.source.span));
|
||||
}
|
||||
}
|
||||
// ESM export * from '...'
|
||||
AstKind::ExportAllDeclaration(export_decl)
|
||||
if export_decl.source.value.as_str().is_some_and(is_parent_import) =>
|
||||
if is_parent_import(export_decl.source.value) =>
|
||||
{
|
||||
ctx.diagnostic(no_relative_parent_imports_diagnostic(export_decl.source.span));
|
||||
}
|
||||
// Dynamic import expressions: import('../foo')
|
||||
AstKind::ImportExpression(import_expr) => {
|
||||
if let Expression::StringLiteral(str_literal) = &import_expr.source
|
||||
&& str_literal.value.as_str().is_some_and(is_parent_import)
|
||||
&& is_parent_import(str_literal.value)
|
||||
{
|
||||
ctx.diagnostic(no_relative_parent_imports_diagnostic(str_literal.span));
|
||||
}
|
||||
@@ -83,7 +84,7 @@ impl Rule for NoRelativeParentImports {
|
||||
// CommonJS require() calls
|
||||
AstKind::CallExpression(call_expr) => {
|
||||
if let Some(str_literal) = call_expr.common_js_require()
|
||||
&& str_literal.value.as_str().is_some_and(is_parent_import)
|
||||
&& is_parent_import(str_literal.value)
|
||||
{
|
||||
ctx.diagnostic(no_relative_parent_imports_diagnostic(str_literal.span));
|
||||
}
|
||||
@@ -95,12 +96,15 @@ impl Rule for NoRelativeParentImports {
|
||||
|
||||
/// Check if the import path is a relative parent import.
|
||||
/// Matches paths like `../foo`, `..`, `./../foo`, etc.
|
||||
fn is_parent_import(path: &str) -> bool {
|
||||
let mut normalized = path;
|
||||
while let Some(rest) = normalized.strip_prefix("./") {
|
||||
normalized = rest;
|
||||
fn is_parent_import(path: JSStr) -> bool {
|
||||
// `.`
|
||||
// and `/` are ASCII, and an ASCII byte never occurs inside a multi-byte WTF-8 sequence,
|
||||
// so the byte view is exact for both tiers.
|
||||
let mut rest = path.as_bytes();
|
||||
while let Some(tail) = rest.strip_prefix(b"./") {
|
||||
rest = tail;
|
||||
}
|
||||
normalized == ".." || normalized.starts_with("../")
|
||||
rest == b".." || rest.starts_with(b"../")
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -108,6 +112,11 @@ fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
// Lone surrogates are part of the value.
|
||||
// A search or prefix check must still see the rest.
|
||||
r#"import foo from "./\uD800""#,
|
||||
r#"import foo from "\uD800/../x""#,
|
||||
r#"require("./\uDC00/..x")"#,
|
||||
// ESLint test cases
|
||||
r#"import foo from "./internal.js""#,
|
||||
r#"import foo from "./app/index.js""#,
|
||||
@@ -127,6 +136,13 @@ fn test() {
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
// Lone surrogates are part of the value.
|
||||
// A search or prefix check must still see the rest.
|
||||
r#"import foo from "../\uD800""#,
|
||||
r#"import foo from "./../\uDC00.js""#,
|
||||
r#"require("../\uD83D\uDE00")"#,
|
||||
r#"import("../\uD800")"#,
|
||||
r#"export * from "..\u002F\uDC00""#,
|
||||
// ESLint test cases
|
||||
r#"import foo from "../plugin.js""#,
|
||||
r#"import foo from "./../plugin.js""#,
|
||||
|
||||
@@ -168,6 +168,10 @@ fn test() {
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
// A source with a lone surrogate matches no allow glob.
|
||||
(r"import './\uD800.css'", None),
|
||||
(r"import './\uD800.css'", Some(json!([{ "allow": ["**"] }]))),
|
||||
(r"require('./\uD800.css')", Some(json!([{ "allow": ["**/*.css"] }]))),
|
||||
("require('should')", None),
|
||||
("import 'foo'", None),
|
||||
("import './styles/app.css'", Some(json!([{ "allow": ["styles/*.css"]}]))),
|
||||
|
||||
@@ -28,6 +28,9 @@ fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
// A segment with a lone surrogate is not `__mocks__`.
|
||||
(r"require('./\uD800__mocks__/x')", None),
|
||||
(r"require('./__mocks__\uDC00/x')", None),
|
||||
("import something from 'something'", None),
|
||||
("require('somethingElse')", None),
|
||||
("require('./__mocks__.js')", None),
|
||||
@@ -41,6 +44,9 @@ fn test() {
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
(r"require('./\uD83D\uDE00/__mocks__')", None),
|
||||
(r"require('./__mocks__/\uD800')", None),
|
||||
(r"require('\uDC00/__mocks__/x')", None),
|
||||
("require('./__mocks__')", None),
|
||||
("require('./__mocks__/')", None),
|
||||
("require('./__mocks__/index')", None),
|
||||
|
||||
@@ -5,10 +5,11 @@ use oxc_ast::{
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::Span;
|
||||
use oxc_str::JSStr;
|
||||
|
||||
use crate::{context::LintContext, rule::Rule, utils::PossibleJestNode};
|
||||
|
||||
fn add_type_parameter_to_module_mock_diagnostic(module_name: &str, span: Span) -> OxcDiagnostic {
|
||||
fn add_type_parameter_to_module_mock_diagnostic(module_name: JSStr, span: Span) -> OxcDiagnostic {
|
||||
OxcDiagnostic::warn(
|
||||
"`jest.mock()` factories should not be used without an explicit type parameter.",
|
||||
)
|
||||
@@ -137,13 +138,17 @@ impl NoUntypedMockFactory {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Expression::StringLiteral(string_literal) = expr
|
||||
&& let Some(value) = string_literal.value.as_str()
|
||||
{
|
||||
if let Expression::StringLiteral(string_literal) = expr {
|
||||
let value = string_literal.value;
|
||||
ctx.diagnostic_with_fix(
|
||||
add_type_parameter_to_module_mock_diagnostic(value, property_span),
|
||||
|fixer| {
|
||||
let mut code = String::with_capacity(string_literal.value.len() + 20);
|
||||
// The fix is built from `&str`, so a name with a lone surrogate is
|
||||
// reported without it.
|
||||
let Some(value) = value.as_str() else {
|
||||
return fixer.noop();
|
||||
};
|
||||
let mut code = String::with_capacity(value.len() + 20);
|
||||
code.push_str("<typeof import('");
|
||||
code.push_str(value);
|
||||
code.push_str("')>");
|
||||
@@ -153,7 +158,7 @@ impl NoUntypedMockFactory {
|
||||
);
|
||||
} else if let Expression::Identifier(ident) = expr {
|
||||
ctx.diagnostic(add_type_parameter_to_module_mock_diagnostic(
|
||||
ident.name.as_str(),
|
||||
JSStr::from(ident.name),
|
||||
property_span,
|
||||
));
|
||||
}
|
||||
@@ -280,6 +285,12 @@ fn test() {
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
// Lone surrogates are part of the value.
|
||||
// A search or prefix check must still see the rest.
|
||||
(r"jest.mock('\uD800', () => ({}))", None),
|
||||
(r"jest.doMock('a\uDC00b', () => ({}))", None),
|
||||
// Ordinary names keep `str` Debug rendering, so an apostrophe is not escaped.
|
||||
(r#"jest.mock("it's", () => ({}))"#, None),
|
||||
(
|
||||
"
|
||||
jest.mock('../moduleName', () => {
|
||||
@@ -333,6 +344,8 @@ fn test() {
|
||||
];
|
||||
|
||||
let fix = vec![
|
||||
// A name with a lone surrogate gets no fix.
|
||||
(r"jest.mock('\uD800', () => ({}))", r"jest.mock('\uD800', () => ({}))", None),
|
||||
(
|
||||
"
|
||||
jest.mock('../moduleName', () => {
|
||||
|
||||
@@ -48,10 +48,12 @@ fn test() {
|
||||
(r"test('\uD800', () => {});", None),
|
||||
(r"test('foo \uD800 bar', () => {});", None),
|
||||
(r"test('\uD800test foo', () => {});", None),
|
||||
// A title with a lone surrogate matches no disallowed word or configured pattern.
|
||||
(
|
||||
r"test('\uD800 correct', () => {});",
|
||||
Some(serde_json::json!([{ "disallowedWords": ["correct"] }])),
|
||||
),
|
||||
(r"test('\uD800 foo', () => {});", Some(serde_json::json!([{ "mustNotMatch": "^\\W" }]))),
|
||||
(
|
||||
"it('correctly sets the value', () => {});",
|
||||
Some(serde_json::json!([
|
||||
@@ -248,6 +250,8 @@ fn test() {
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
// A title with a lone surrogate matches no configured pattern.
|
||||
(r"test('\uD800 foo', () => {});", Some(serde_json::json!([{ "mustMatch": "^\\W" }]))),
|
||||
("const String = { raw: () => 'foo' }; it(String.raw`foo`, () => {})", None),
|
||||
(
|
||||
"test('the correct way to properly handle all things', () => {});",
|
||||
|
||||
@@ -11,7 +11,7 @@ use oxc_ast::{
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::{GetSpan, Span};
|
||||
use oxc_str::CompactStr;
|
||||
use oxc_str::{CompactStr, JSStr};
|
||||
|
||||
use crate::{
|
||||
AstNode,
|
||||
@@ -250,19 +250,17 @@ impl AnchorIsValid {
|
||||
fn check_value(value: &JSXAttributeValue) -> HrefValueKind {
|
||||
match value {
|
||||
JSXAttributeValue::Element(_) => HrefValueKind::Valid,
|
||||
JSXAttributeValue::StringLiteral(str_lit) => str_lit
|
||||
.value
|
||||
.as_str()
|
||||
.map_or(HrefValueKind::Valid, Self::href_value_kind_from_string),
|
||||
JSXAttributeValue::StringLiteral(str_lit) => {
|
||||
Self::href_value_kind_from_string(str_lit.value)
|
||||
}
|
||||
JSXAttributeValue::ExpressionContainer(exp) => match &exp.expression {
|
||||
JSXExpression::Identifier(ident) if ident.name == "undefined" => {
|
||||
HrefValueKind::Nullish
|
||||
}
|
||||
JSXExpression::NullLiteral(_) => HrefValueKind::Nullish,
|
||||
JSXExpression::StringLiteral(str_lit) => str_lit
|
||||
.value
|
||||
.as_str()
|
||||
.map_or(HrefValueKind::Valid, Self::href_value_kind_from_string),
|
||||
JSXExpression::StringLiteral(str_lit) => {
|
||||
Self::href_value_kind_from_string(str_lit.value)
|
||||
}
|
||||
JSXExpression::TemplateLiteral(temp_lit) => {
|
||||
if !temp_lit.expressions.is_empty() {
|
||||
return HrefValueKind::Valid;
|
||||
@@ -271,7 +269,7 @@ impl AnchorIsValid {
|
||||
let Some(quasi) = temp_lit.single_quasi() else {
|
||||
return HrefValueKind::Valid;
|
||||
};
|
||||
quasi.as_str().map_or(HrefValueKind::Valid, Self::href_value_kind_from_string)
|
||||
Self::href_value_kind_from_string(quasi)
|
||||
}
|
||||
_ => HrefValueKind::Valid,
|
||||
},
|
||||
@@ -279,14 +277,15 @@ impl AnchorIsValid {
|
||||
}
|
||||
}
|
||||
|
||||
fn href_value_kind_from_string(href: &str) -> HrefValueKind {
|
||||
fn href_value_kind_from_string(href: JSStr<'_>) -> HrefValueKind {
|
||||
if Self::is_invalid_href(href) { HrefValueKind::Invalid } else { HrefValueKind::Valid }
|
||||
}
|
||||
|
||||
fn is_invalid_href(href: &str) -> bool {
|
||||
let href_without_leading_non_word =
|
||||
href.trim_start_matches(|c: char| !c.is_ascii_alphanumeric() && c != '_');
|
||||
href.is_empty() || href == "#" || href_without_leading_non_word.starts_with("javascript:")
|
||||
fn is_invalid_href(href: JSStr<'_>) -> bool {
|
||||
// A lone surrogate is a non-word character, and the predicate never matches one.
|
||||
let word_start =
|
||||
href.find(|c: char| c.is_ascii_alphanumeric() || c == '_').unwrap_or(href.len());
|
||||
href.is_empty() || href == "#" || href.as_bytes()[word_start..].starts_with(b"javascript:")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,6 +592,8 @@ fn test() {
|
||||
(r"<a href='#' onClick={() => void 0} />", None, None),
|
||||
(r"<a href='javascript:void(0)' onClick={() => void 0} />", None, None),
|
||||
(r"<a href={'javascript:void(0)'} onClick={() => void 0} />", None, None),
|
||||
(r#"<a href={"javascript:void(0)\uD800"} />"#, None, None),
|
||||
(r"<a href={`\uD800javascript:void(0)`} />", None, None),
|
||||
(r"<Link />", Some(components()), None),
|
||||
(r"<Link href={undefined} />", Some(components()), None),
|
||||
(r"<Link href={null} />", Some(components()), None),
|
||||
|
||||
@@ -8,7 +8,7 @@ use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_ecmascript::{ToBoolean, WithoutGlobalReferenceInformation};
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::Span;
|
||||
use oxc_str::CompactStr;
|
||||
use oxc_str::{CompactStr, JSStr};
|
||||
use oxc_syntax::operator::UnaryOperator;
|
||||
|
||||
use crate::{
|
||||
@@ -157,7 +157,8 @@ fn is_valid_value_for_aria_prop_type(
|
||||
{
|
||||
return true;
|
||||
}
|
||||
parse_aria_prop_value_as_string(value, false).is_some()
|
||||
// Any string is valid, including one containing a lone surrogate.
|
||||
string_literal_value(value).is_some()
|
||||
}
|
||||
AriaPropType::Integer | AriaPropType::Number => {
|
||||
if let Some(value_string) = parse_aria_prop_value_as_string(value, false) {
|
||||
@@ -180,11 +181,13 @@ fn is_valid_value_for_aria_prop_type(
|
||||
return true;
|
||||
}
|
||||
|
||||
let Some(value_string) = parse_aria_prop_value_as_string(value, false) else {
|
||||
let Some(value_string) = string_literal_value(value) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
value_string.split_whitespace().next().is_some()
|
||||
// At least one ID: a code point that is not whitespace.
|
||||
// A lone surrogate is not whitespace.
|
||||
value_string.chars().any(|c| c.to_char().is_none_or(|c| !c.is_whitespace()))
|
||||
}
|
||||
AriaPropType::Token(valid_tokens) => {
|
||||
let Some(value_string) = parse_aria_prop_value_as_string(value, true) else {
|
||||
@@ -209,6 +212,22 @@ fn is_valid_value_for_aria_prop_type(
|
||||
}
|
||||
}
|
||||
|
||||
/// The value of a string-shaped attribute:
|
||||
/// * A string literal.
|
||||
/// * A string literal in an expression container.
|
||||
/// * A template literal without substitutions.
|
||||
fn string_literal_value<'a>(value: &JSXAttributeValue<'a>) -> Option<JSStr<'a>> {
|
||||
match value {
|
||||
JSXAttributeValue::StringLiteral(string_lit) => Some(string_lit.value),
|
||||
JSXAttributeValue::ExpressionContainer(container) => match &container.expression {
|
||||
JSXExpression::StringLiteral(string_lit) => Some(string_lit.value),
|
||||
JSXExpression::TemplateLiteral(template_lit) => template_lit.single_quasi(),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_aria_prop_value_as_string(
|
||||
value: &JSXAttributeValue,
|
||||
boolean_as_string: bool, // whether to convert boolean literal to string
|
||||
@@ -409,6 +428,13 @@ fn test() {
|
||||
"<div aria-label={`Hello ${foo}`} />",
|
||||
"<div aria-label={`foo-${id}`} />",
|
||||
"<div aria-label={`foo-${id} bar`} />",
|
||||
// A string containing a lone surrogate is still a string.
|
||||
r#"<div aria-label={"\uD800"} />"#,
|
||||
r#"<div aria-label={"\uDC00 close"} />"#,
|
||||
r"<div aria-label={`\uD83D\uDE00`} />",
|
||||
r#"<div aria-labelledby={"\uD800"} />"#,
|
||||
r#"<div aria-labelledby={"a \uDC00"} />"#,
|
||||
r"<div aria-controls={`\uD83D\uDE00`} />",
|
||||
"<div aria-label={foo} />",
|
||||
"<div aria-label={foo.bar} />",
|
||||
"<div aria-label={null} />",
|
||||
@@ -538,6 +564,10 @@ fn test() {
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
// A lone surrogate never matches a token.
|
||||
r#"<div aria-invalid={"\uD800"} />"#,
|
||||
r#"<div aria-checked={"true\uDC00"} />"#,
|
||||
r#"<div aria-level={"\uD83D\uDE00"} />"#,
|
||||
r#"<div aria-hidden="yes" />"#,
|
||||
r#"<div aria-hidden="no" />"#,
|
||||
"<div aria-hidden={1234} />",
|
||||
|
||||
@@ -165,25 +165,19 @@ impl Rule for ImgRedundantAlt {
|
||||
|
||||
match alt_attribute {
|
||||
JSXAttributeValue::StringLiteral(lit) => {
|
||||
let alt_text = lit.value.as_str();
|
||||
|
||||
if alt_text.is_some_and(|text| self.is_redundant_alt_text(text)) {
|
||||
if self.is_redundant_alt_text(lit.value.as_bytes()) {
|
||||
ctx.diagnostic(img_redundant_alt_diagnostic(alt_attribute_name_span));
|
||||
}
|
||||
}
|
||||
JSXAttributeValue::ExpressionContainer(container) => match &container.expression {
|
||||
JSXExpression::StringLiteral(lit) => {
|
||||
let alt_text = lit.value.as_str();
|
||||
|
||||
if alt_text.is_some_and(|text| self.is_redundant_alt_text(text)) {
|
||||
if self.is_redundant_alt_text(lit.value.as_bytes()) {
|
||||
ctx.diagnostic(img_redundant_alt_diagnostic(alt_attribute_name_span));
|
||||
}
|
||||
}
|
||||
JSXExpression::TemplateLiteral(lit) => {
|
||||
for quasi in &lit.quasis {
|
||||
let alt_text = quasi.value.raw.as_str();
|
||||
|
||||
if self.is_redundant_alt_text(alt_text) {
|
||||
if self.is_redundant_alt_text(quasi.value.raw.as_bytes()) {
|
||||
ctx.diagnostic(img_redundant_alt_diagnostic(alt_attribute_name_span));
|
||||
}
|
||||
}
|
||||
@@ -203,20 +197,29 @@ impl ImgRedundantAlt {
|
||||
starts_boundary && ends_boundary
|
||||
}
|
||||
|
||||
/// Whether `alt_text` contains a configured word, ignoring ASCII case.
|
||||
///
|
||||
/// `alt_text` may be WTF-8. A word is UTF-8, so it never matches part of a lone surrogate,
|
||||
/// and a lone surrogate is not a word character.
|
||||
#[inline]
|
||||
fn is_redundant_alt_text(&self, alt_text: &str) -> bool {
|
||||
let alt_text = alt_text.cow_to_ascii_lowercase();
|
||||
let alt_text_bytes = alt_text.as_bytes();
|
||||
fn is_redundant_alt_text(&self, alt_text: &[u8]) -> bool {
|
||||
let lowercase;
|
||||
let alt_text = if alt_text.iter().any(u8::is_ascii_uppercase) {
|
||||
lowercase = alt_text.to_ascii_lowercase();
|
||||
lowercase.as_slice()
|
||||
} else {
|
||||
alt_text
|
||||
};
|
||||
|
||||
for word in &self.words {
|
||||
for (index, _) in alt_text.match_indices(word.as_ref()) {
|
||||
let end = index + word.len();
|
||||
if Self::is_word_boundary(alt_text_bytes, index, end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
self.words.iter().any(|word| {
|
||||
memchr::memmem::find_iter(alt_text, word.as_bytes()).any(|index| {
|
||||
// Like `str::match_indices`, match only at a character boundary. Only an empty
|
||||
// word can match elsewhere.
|
||||
let at_char_boundary =
|
||||
alt_text.get(index).is_none_or(|byte| !matches!(byte, 0x80..=0xBF));
|
||||
at_char_boundary && Self::is_word_boundary(alt_text, index, index + word.len())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,6 +246,13 @@ fn test() {
|
||||
|
||||
let pass = vec![
|
||||
(r"<img alt='foo' />;", None, None),
|
||||
// A lone surrogate is not part of a word.
|
||||
(r#"<img alt={"\uD800"} />;"#, None, None),
|
||||
// An empty word matches only at character boundaries.
|
||||
(r#"<img alt="aéb" />;"#, Some(serde_json::json!([{ "words": [""] }])), None),
|
||||
(r#"<img alt={"a\uD800b"} />;"#, Some(serde_json::json!([{ "words": [""] }])), None),
|
||||
(r#"<img alt={"\uD800 imagery"} />;"#, None, None),
|
||||
(r#"<img alt={"photos\uDC00"} />;"#, None, None),
|
||||
(r"<img alt='picture of me taking a photo of an image' aria-hidden />", None, None),
|
||||
(r"<img aria-hidden alt='photo of image' />", None, None),
|
||||
(r"<img ALt='foo' />;", None, None),
|
||||
@@ -280,6 +290,11 @@ fn test() {
|
||||
|
||||
let fail = vec![
|
||||
(r"<img alt='Photo of friend.' />;", None, None),
|
||||
(r#"<img alt={"\uD83D\uDE00 picture"} />;"#, None, None),
|
||||
// A lone surrogate is not part of a word.
|
||||
(r#"<img alt={"image \uD800"} />;"#, None, None),
|
||||
(r#"<img alt={"\uDC00 Photo"} />;"#, None, None),
|
||||
(r#"<img alt={"\uD800image"} />;"#, None, None),
|
||||
(r"<img alt='Picture of friend.' />;", None, None),
|
||||
(r"<img alt='Image of friend.' />;", None, None),
|
||||
(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::borrow::Cow;
|
||||
use std::{borrow::Cow, iter};
|
||||
|
||||
use cow_utils::CowUtils;
|
||||
use oxc_ast::{
|
||||
@@ -8,7 +8,7 @@ use oxc_ast::{
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::{GetSpan, Span};
|
||||
use oxc_str::CompactStr;
|
||||
use oxc_str::{CompactStr, JSChar, JSStr};
|
||||
use rustc_hash::FxHashMap;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
@@ -167,7 +167,7 @@ impl Rule for NoNoninteractiveElementInteractions {
|
||||
let role_value = role_value(jsx_el);
|
||||
if is_content_editable(jsx_el)
|
||||
|| is_hidden_from_screen_reader(ctx, jsx_el)
|
||||
|| role_value.is_some_and(|role| {
|
||||
|| role_value.and_then(JSStr::as_str).is_some_and(|role| {
|
||||
let role = role.cow_to_lowercase();
|
||||
matches!(role.as_ref(), "presentation" | "none")
|
||||
})
|
||||
@@ -175,7 +175,7 @@ impl Rule for NoNoninteractiveElementInteractions {
|
||||
return;
|
||||
}
|
||||
|
||||
if role_value.is_some_and(|role| {
|
||||
if role_value.and_then(JSStr::as_str).is_some_and(|role| {
|
||||
let role = role.cow_to_lowercase();
|
||||
is_abstract_role_name(role.as_ref())
|
||||
}) {
|
||||
@@ -254,12 +254,12 @@ fn is_content_editable(jsx_el: &JSXOpeningElement) -> bool {
|
||||
.is_some_and(|value| value == "true")
|
||||
}
|
||||
|
||||
fn role_value<'b>(jsx_el: &'b JSXOpeningElement<'_>) -> Option<&'b str> {
|
||||
fn role_value<'a>(jsx_el: &JSXOpeningElement<'a>) -> Option<JSStr<'a>> {
|
||||
has_jsx_prop_ignore_case(jsx_el, "role").and_then(get_prop_value).and_then(
|
||||
|value| match value {
|
||||
JSXAttributeValue::StringLiteral(role) => role.value.as_str(),
|
||||
JSXAttributeValue::StringLiteral(role) => Some(role.value),
|
||||
JSXAttributeValue::ExpressionContainer(container) => match &container.expression {
|
||||
JSXExpression::StringLiteral(role) => role.value.as_str(),
|
||||
JSXExpression::StringLiteral(role) => Some(role.value),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
@@ -315,11 +315,28 @@ fn is_focusable(jsx_el: &JSXOpeningElement, element_type: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
fn first_recognized_role(role_value: &str) -> Option<Cow<'_, str>> {
|
||||
role_value.split_whitespace().find_map(|role| {
|
||||
let role = role.cow_to_lowercase();
|
||||
is_recognized_role(role.as_ref()).then_some(role)
|
||||
})
|
||||
/// Split the value on whitespace like `str::split_whitespace`
|
||||
/// and return the first token that is a recognized role.
|
||||
fn first_recognized_role(role_value: JSStr<'_>) -> Option<Cow<'_, str>> {
|
||||
let bytes = role_value.as_bytes();
|
||||
let mut token_start = 0;
|
||||
let mut offset = 0;
|
||||
for c in role_value.chars().chain(iter::once(JSChar::from(' '))) {
|
||||
if c.to_char().is_some_and(char::is_whitespace) {
|
||||
// A token with a lone surrogate is not UTF-8 and is never a recognized role.
|
||||
if let Ok(token) = std::str::from_utf8(&bytes[token_start..offset])
|
||||
&& !token.is_empty()
|
||||
{
|
||||
let role = token.cow_to_lowercase();
|
||||
if is_recognized_role(role.as_ref()) {
|
||||
return Some(role);
|
||||
}
|
||||
}
|
||||
token_start = offset + c.len_bytes();
|
||||
}
|
||||
offset += c.len_bytes();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn is_recognized_role(role: &str) -> bool {
|
||||
@@ -401,6 +418,7 @@ fn test() {
|
||||
(r#"<div role="presentation" onClick={() => void 0} />"#, None, None).into(),
|
||||
(r#"<main role={"button"} onClick={() => void 0} />"#, None, None).into(),
|
||||
(r#"<main role="unknown button" onClick={() => void 0} />"#, None, None).into(),
|
||||
(r#"<li onClick={() => void 0} role={"\uD800 button"} />"#, None, None).into(),
|
||||
(
|
||||
r#"<div role="separator" tabIndex={0} aria-valuenow={50} onClick={() => void 0} />"#,
|
||||
None,
|
||||
|
||||
@@ -9,7 +9,7 @@ use oxc_ast::{
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::Span;
|
||||
use oxc_str::CompactStr;
|
||||
use oxc_str::{CompactStr, JSStr};
|
||||
|
||||
use crate::{
|
||||
AstNode,
|
||||
@@ -197,14 +197,14 @@ fn get_select_implicit_role(jsx_el: &JSXOpeningElement) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_static_string_prop_value<'a>(item: &'a JSXAttributeItem<'_>) -> Option<&'a str> {
|
||||
fn get_static_string_prop_value<'a>(item: &JSXAttributeItem<'a>) -> Option<JSStr<'a>> {
|
||||
match get_prop_value(item)? {
|
||||
JSXAttributeValue::StringLiteral(lit) => lit.value.as_str(),
|
||||
JSXAttributeValue::StringLiteral(lit) => Some(lit.value),
|
||||
JSXAttributeValue::ExpressionContainer(container) => {
|
||||
let Expression::StringLiteral(lit) = container.expression.as_expression()? else {
|
||||
return None;
|
||||
};
|
||||
lit.value.as_str()
|
||||
Some(lit.value)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
@@ -241,6 +241,7 @@ fn get_input_implicit_role(
|
||||
) -> Option<&'static str> {
|
||||
let input_type = has_jsx_prop_ignore_case(jsx_el, "type")
|
||||
.and_then(get_static_string_prop_value)
|
||||
.and_then(JSStr::as_str)
|
||||
.unwrap_or("text");
|
||||
let has_list = has_jsx_prop_ignore_case(jsx_el, "list").is_some();
|
||||
|
||||
@@ -351,6 +352,7 @@ fn test() {
|
||||
Some(serde_json::json!([{ "ul": ["list"], "ol": ["list"] }])),
|
||||
None,
|
||||
),
|
||||
(r#"<img src={"a\uD800.svg"} role="img" />"#, None, None),
|
||||
(r#"<li role="listitem" />"#, Some(serde_json::json!([{ "li": ["listitem"] }])), None),
|
||||
// Ancestor-dependent elements: implicit role cannot be determined
|
||||
// from a single opening element, so do not flag. See #22743.
|
||||
|
||||
@@ -235,14 +235,12 @@ fn infer_module_type(init: &Expression) -> ModuleType {
|
||||
return ModuleType::Computed;
|
||||
};
|
||||
|
||||
let Some(value) = arg.as_expression().and_then(|expr| match expr {
|
||||
Expression::StringLiteral(lit) => lit.value.as_str(),
|
||||
_ => None,
|
||||
}) else {
|
||||
let Some(Expression::StringLiteral(lit)) = arg.as_expression() else {
|
||||
return ModuleType::Computed;
|
||||
};
|
||||
let value = lit.value;
|
||||
|
||||
if BUILTIN_MODULES.contains(&value) {
|
||||
if value.as_str().is_some_and(|value| BUILTIN_MODULES.contains(&value)) {
|
||||
return ModuleType::Core;
|
||||
}
|
||||
|
||||
@@ -309,6 +307,9 @@ fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
// A path with a lone surrogate is still classified as a file or a module.
|
||||
(r"var a = require('./\uD800'), b = require('../x')", Some(serde_json::json!([true]))),
|
||||
(r"var a = require('\uDC00'), b = require('foo')", Some(serde_json::json!([true]))),
|
||||
("var a, b = 42, c = doStuff()", Some(serde_json::json!([false]))),
|
||||
(
|
||||
"var a = require(42), b = require(), c = require('y'), d = require(doStuff())",
|
||||
@@ -340,6 +341,9 @@ fn test() {
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
// A path with a lone surrogate is still classified as a file or a module.
|
||||
(r"var fs = require('fs'), a = require('\uD800')", Some(serde_json::json!([true]))),
|
||||
(r"var a = require('./\uD83D\uDE00'), b = require('foo')", Some(serde_json::json!([true]))),
|
||||
("var fs = require('fs'), foo = 42", Some(serde_json::json!([false]))),
|
||||
("var fs = require('fs'), foo", Some(serde_json::json!([false]))),
|
||||
(
|
||||
|
||||
@@ -16,6 +16,7 @@ use oxc_ast::{
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::{GetSpan, Span};
|
||||
use oxc_str::JSStr;
|
||||
|
||||
use crate::{
|
||||
AstNode,
|
||||
@@ -46,7 +47,8 @@ fn key_prop_must_be_placed_before_spread(span: Span) -> OxcDiagnostic {
|
||||
.with_label(span)
|
||||
}
|
||||
|
||||
fn duplicate_key_prop(key_value: &str, span: Span) -> OxcDiagnostic {
|
||||
fn duplicate_key_prop(key_value: JSStr<'_>, span: Span) -> OxcDiagnostic {
|
||||
let key_value = key_value.display();
|
||||
OxcDiagnostic::warn(format!("Duplicate key '{key_value}' found in JSX elements"))
|
||||
.with_help("Each child in a list should have a unique 'key' prop")
|
||||
.with_label(span)
|
||||
@@ -390,7 +392,10 @@ fn gen_diagnostic(span: Span, outer: &InsideArrayOrIterator) -> OxcDiagnostic {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_jsx_element_key_value(jsx_elem: &JSXElement) -> Option<(String, Span)> {
|
||||
fn get_jsx_element_key_value<'a>(
|
||||
jsx_elem: &JSXElement<'a>,
|
||||
ctx: &LintContext<'a>,
|
||||
) -> Option<(JSStr<'a>, Span)> {
|
||||
for attr in &jsx_elem.opening_element.attributes {
|
||||
if let JSXAttributeItem::Attribute(attr) = attr
|
||||
&& let JSXAttributeName::Identifier(ident) = &attr.name
|
||||
@@ -400,21 +405,21 @@ fn get_jsx_element_key_value(jsx_elem: &JSXElement) -> Option<(String, Span)> {
|
||||
if let Some(value) = &attr.value {
|
||||
match value {
|
||||
JSXAttributeValue::StringLiteral(lit) => {
|
||||
return Some((lit.value.as_str()?.to_owned(), attr.span));
|
||||
return Some((lit.value, attr.span));
|
||||
}
|
||||
JSXAttributeValue::ExpressionContainer(container) => {
|
||||
// JSXExpression inherits from Expression, so we match the Expression variants directly
|
||||
match &container.expression {
|
||||
JSXExpression::StringLiteral(lit) => {
|
||||
return Some((lit.value.as_str()?.to_owned(), attr.span));
|
||||
return Some((lit.value, attr.span));
|
||||
}
|
||||
JSXExpression::NumericLiteral(lit) => {
|
||||
return Some((lit.value.to_string(), attr.span));
|
||||
let value =
|
||||
JSStr::from_str_in(&lit.value.to_string(), &ctx.allocator());
|
||||
return Some((value, attr.span));
|
||||
}
|
||||
JSXExpression::TemplateLiteral(lit)
|
||||
if lit.expressions.is_empty() && lit.quasis.len() == 1 =>
|
||||
{
|
||||
return Some((lit.quasis[0].value.raw.to_string(), attr.span));
|
||||
JSXExpression::TemplateLiteral(lit) => {
|
||||
return lit.single_quasi().map(|value| (value, attr.span));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -428,28 +433,28 @@ fn get_jsx_element_key_value(jsx_elem: &JSXElement) -> Option<(String, Span)> {
|
||||
}
|
||||
|
||||
fn check_duplicate_keys_in_array<'a>(array_expr: &ArrayExpression<'a>, ctx: &LintContext<'a>) {
|
||||
let mut seen_keys: FxHashSet<String> = FxHashSet::default();
|
||||
let mut seen_keys: FxHashSet<JSStr<'a>> = FxHashSet::default();
|
||||
|
||||
for element in &array_expr.elements {
|
||||
// ArrayExpressionElement also inherits from Expression
|
||||
if let ArrayExpressionElement::JSXElement(jsx_elem) = element
|
||||
&& let Some((key_value, span)) = get_jsx_element_key_value(jsx_elem)
|
||||
&& let Some(key_value) = seen_keys.replace(key_value)
|
||||
&& let Some((key_value, span)) = get_jsx_element_key_value(jsx_elem, ctx)
|
||||
&& !seen_keys.insert(key_value)
|
||||
{
|
||||
ctx.diagnostic(duplicate_key_prop(&key_value, span));
|
||||
ctx.diagnostic(duplicate_key_prop(key_value, span));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_duplicate_keys_in_children<'a>(jsx_elem: &JSXElement<'a>, ctx: &LintContext<'a>) {
|
||||
let mut seen_keys: FxHashSet<String> = FxHashSet::default();
|
||||
let mut seen_keys: FxHashSet<JSStr<'a>> = FxHashSet::default();
|
||||
|
||||
for child in &jsx_elem.children {
|
||||
if let JSXChild::Element(child_elem) = child
|
||||
&& let Some((key_value, span)) = get_jsx_element_key_value(child_elem)
|
||||
&& let Some(key_value) = seen_keys.replace(key_value)
|
||||
&& let Some((key_value, span)) = get_jsx_element_key_value(child_elem, ctx)
|
||||
&& !seen_keys.insert(key_value)
|
||||
{
|
||||
ctx.diagnostic(duplicate_key_prop(&key_value, span));
|
||||
ctx.diagnostic(duplicate_key_prop(key_value, span));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -467,6 +472,17 @@ fn test() {
|
||||
(r"[1, 2, 3].map(x => <App key={x} />);", None, None),
|
||||
(r"[1, 2 ,3].map(x => x && <App x={x} key={x} />);", None, None),
|
||||
(r#"[1, 2 ,3].map(x => x ? <App x={x} key="1" /> : <OtherApp x={x} key="2" />);"#, None, None),
|
||||
// Different lone surrogates, and a pair versus its lead, are different keys.
|
||||
(
|
||||
r#"const spans = [<span key={"\uD800"}/>, <span key={"\uDC00"}/>];"#,
|
||||
Some(serde_json::json!([{ "warnOnDuplicates": true }])),
|
||||
None,
|
||||
),
|
||||
(
|
||||
r#"const div = <div><span key={"\uD83D\uDE00"}/><span key={"\uD83D"}/></div>;"#,
|
||||
Some(serde_json::json!([{ "warnOnDuplicates": true }])),
|
||||
None,
|
||||
),
|
||||
(r"[1, 2, 3].map(x => { return <App key={x} /> });", None, None),
|
||||
(r"Array.from([1, 2, 3], function(x) { return <App key={x} /> });", None, None),
|
||||
(r"Array.from([1, 2, 3], (x => <App key={x} />));", None, None),
|
||||
@@ -600,6 +616,16 @@ fn test() {
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
(
|
||||
r#"const xs = [<span key={"a\uD800"} />, <span key={`a\uD800`} />];"#,
|
||||
Some(serde_json::json!([{ "warnOnDuplicates": true }])),
|
||||
None,
|
||||
),
|
||||
(
|
||||
r#"const xs = [<span key={"foo"} />, <span key={`\u0066oo`} />];"#,
|
||||
Some(serde_json::json!([{ "warnOnDuplicates": true }])),
|
||||
None,
|
||||
),
|
||||
("[<App />];", None, None),
|
||||
("[<App {...key} />];", None, None),
|
||||
("[<App key={0}/>, <App />];", None, None),
|
||||
@@ -652,6 +678,22 @@ fn test() {
|
||||
Some(serde_json::json!([{ "warnOnDuplicates": true }])),
|
||||
None,
|
||||
),
|
||||
// Keys containing lone surrogates are compared by their JavaScript value.
|
||||
(
|
||||
r#"const spans = [<span key={"\uD800"}/>, <span key={"\uD800"}/>];"#,
|
||||
Some(serde_json::json!([{ "warnOnDuplicates": true }])),
|
||||
None,
|
||||
),
|
||||
(
|
||||
r"const spans = [<span key={`a\uDC00`}/>, <span key={`a\uDC00`}/>];",
|
||||
Some(serde_json::json!([{ "warnOnDuplicates": true }])),
|
||||
None,
|
||||
),
|
||||
(
|
||||
r#"const div = <div><span key={"\uD83D\uDE00"}/><span key={"\uD83D\uDE00"}/></div>;"#,
|
||||
Some(serde_json::json!([{ "warnOnDuplicates": true }])),
|
||||
None,
|
||||
),
|
||||
(
|
||||
r#"
|
||||
const div = (
|
||||
|
||||
@@ -11,6 +11,7 @@ use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::{GetSpan, Span};
|
||||
use oxc_str::CompactStr;
|
||||
use oxc_str::JSStr;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -350,7 +351,7 @@ fn append_noreferrer(raw: &str) -> String {
|
||||
format!("{quote}{}{quote}", parts.join(" "))
|
||||
}
|
||||
|
||||
fn check_is_external_link(link: &str) -> bool {
|
||||
fn check_is_external_link(link: JSStr<'_>) -> bool {
|
||||
link.contains("//")
|
||||
}
|
||||
|
||||
@@ -361,7 +362,7 @@ fn match_href_expression(
|
||||
) {
|
||||
match expr {
|
||||
Expression::StringLiteral(str) => {
|
||||
*is_external_link = str.value.as_str().is_some_and(check_is_external_link);
|
||||
*is_external_link = check_is_external_link(str.value);
|
||||
}
|
||||
Expression::Identifier(_) => *is_dynamic_link = true,
|
||||
Expression::ConditionalExpression(expr) => {
|
||||
@@ -382,7 +383,7 @@ fn check_href(
|
||||
matches!(enforce_dynamic_links, EnforceDynamicLinksEnum::Never);
|
||||
match attribute_value {
|
||||
JSXAttributeValue::StringLiteral(str) => {
|
||||
is_external_link = str.value.as_str().is_some_and(check_is_external_link);
|
||||
is_external_link = check_is_external_link(str.value);
|
||||
}
|
||||
JSXAttributeValue::ExpressionContainer(expr) => {
|
||||
if let Some(expr) = expr.expression.as_expression() {
|
||||
@@ -408,22 +409,17 @@ fn check_href(
|
||||
}
|
||||
|
||||
fn check_rel_val(str: &StringLiteral, allow_referrer: bool) -> bool {
|
||||
let Some(value) = str.value.as_str() else {
|
||||
return false;
|
||||
};
|
||||
let mut splits = value.split(' ');
|
||||
if allow_referrer {
|
||||
return splits.any(|str| {
|
||||
if str == "noopener" {
|
||||
return true;
|
||||
}
|
||||
if str == "noreferrer" {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
});
|
||||
return any_rel_token(str.value, |token| token == b"noopener" || token == b"noreferrer");
|
||||
}
|
||||
splits.any(|str| str.eq_ignore_ascii_case("noreferrer"))
|
||||
any_rel_token(str.value, |token| token.eq_ignore_ascii_case(b"noreferrer"))
|
||||
}
|
||||
|
||||
/// Split `rel` on spaces and test each token.
|
||||
/// The space is ASCII, so splitting the WTF-8 bytes finds the same tokens.
|
||||
/// A token with a lone surrogate never equals an ASCII keyword.
|
||||
fn any_rel_token(value: JSStr<'_>, is_match: impl FnMut(&[u8]) -> bool) -> bool {
|
||||
value.as_bytes().split(|&byte| byte == b' ').any(is_match)
|
||||
}
|
||||
|
||||
fn match_rel_expression<'a>(
|
||||
@@ -517,6 +513,19 @@ fn test() {
|
||||
(r#"<a href="foobar" target="_blank" rel={"noreferrer"}></a>"#, None, None),
|
||||
(r#"<a href={"foobar"} target={"_blank"} rel={"noopener noreferrer"}></a>"#, None, None),
|
||||
(r#"<a href={"foobar"} target={"_blank"} rel={"noreferrer"}></a>"#, None, None),
|
||||
// A lone surrogate in `rel` does not hide the tokens beside it.
|
||||
(
|
||||
r#"<a href={"https://x/\uD800"} target="_blank" rel={"\uD800 noreferrer"}></a>"#,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
(r#"<a href={"//x/\uDC00"} target="_blank" rel={"noreferrer \uDC00"}></a>"#, None, None),
|
||||
(
|
||||
r#"<a href={"https://x/\uD83D\uDE00"} target="_blank" rel={"noopener \uD83D noreferrer"}></a>"#,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
(r#"<a href={"\uD800"} target="_blank"></a>"#, None, None),
|
||||
(r"<a href={'foobar'} target={'_blank'} rel={'noopener noreferrer'}></a>", None, None),
|
||||
(r"<a href={'foobar'} target={'_blank'} rel={'noreferrer'}></a>", None, None),
|
||||
(r"<a href={`foobar`} target={`_blank`} rel={`noopener noreferrer`}></a>", None, None),
|
||||
@@ -704,6 +713,16 @@ fn test() {
|
||||
|
||||
let fail = vec![
|
||||
(r#"<a target="_blank" href="https://example.com/1"></a>"#, None, None),
|
||||
// A lone surrogate in the link does not make it internal.
|
||||
(r#"<a target="_blank" href={"https://x/\uD800"}></a>"#, None, None),
|
||||
// A token with a lone surrogate is not `noreferrer`.
|
||||
(r#"<a target="_blank" href="http://x.com" rel={"\uD800"}></a>"#, None, None),
|
||||
(r#"<a target="_blank" href={"//x/\uDC00"} rel={"\uDC00"}></a>"#, None, None),
|
||||
(
|
||||
r#"<a target="_blank" href={"https://x/\uD83D\uDE00"} rel={"noreferrer\uD800"}></a>"#,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
(r#"<a target="_blank" rel="" href="https://example.com/2"></a>"#, None, None),
|
||||
(r#"<a target="_blank" rel={0} href="https://example.com/3"></a>"#, None, None),
|
||||
(r#"<a target="_blank" rel={1} href="https://example.com/3"></a>"#, None, None),
|
||||
|
||||
@@ -8,7 +8,7 @@ use oxc_span::Span;
|
||||
|
||||
use crate::{AstNode, context::LintContext, rule::Rule, utils::is_create_element_call};
|
||||
|
||||
fn no_namespace_diagnostic(span: Span, component_name: &str) -> OxcDiagnostic {
|
||||
fn no_namespace_diagnostic(span: Span, component_name: impl std::fmt::Display) -> OxcDiagnostic {
|
||||
let message = format!(
|
||||
r"React component {component_name} must not be in a namespace, as React does not support them."
|
||||
);
|
||||
@@ -67,10 +67,9 @@ impl Rule for NoNamespace {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(value) = str_lit.value.as_str()
|
||||
&& value.contains(':')
|
||||
{
|
||||
ctx.diagnostic(no_namespace_diagnostic(str_lit.span, value));
|
||||
let value = str_lit.value;
|
||||
if value.contains(':') {
|
||||
ctx.diagnostic(no_namespace_diagnostic(str_lit.span, value.display()));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -108,6 +107,8 @@ fn test() {
|
||||
"<Object.TestComponent />",
|
||||
r#"React.createElement("Object.TestComponent")"#,
|
||||
"React.createElement(null)",
|
||||
r#"React.createElement("a\uD800b")"#,
|
||||
r#"React.createElement("\uDC00")"#,
|
||||
"React.createElement(true)",
|
||||
"React.createElement({})",
|
||||
];
|
||||
@@ -115,6 +116,10 @@ fn test() {
|
||||
let fail = vec![
|
||||
"<ns:testcomponent />",
|
||||
r#"React.createElement("ns:testcomponent")"#,
|
||||
// Lone surrogates beside the namespace separator are still namespaced names.
|
||||
r#"React.createElement("ns:\uD800")"#,
|
||||
r#"React.createElement("\uDC00:testcomponent")"#,
|
||||
r#"React.createElement("\uD83D\uDE00:\uD800")"#,
|
||||
"<ns:testComponent />",
|
||||
r#"React.createElement("ns:testComponent")"#,
|
||||
"<ns:test_component />",
|
||||
|
||||
@@ -282,6 +282,7 @@ impl NoLargeSnapshotsConfig {
|
||||
member_expr: &MemberExpression,
|
||||
ctx: &LintContext,
|
||||
) -> bool {
|
||||
// A snapshot name with a lone surrogate matches no allowed pattern.
|
||||
let Some(snapshot_name) = member_expr.static_property_name().and_then(JSStr::as_str) else {
|
||||
return false;
|
||||
};
|
||||
@@ -324,6 +325,64 @@ mod test {
|
||||
assert!(!matchers[1].is_match("snapshot literal"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_snapshot_allow_list_requires_a_match() {
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_ast::AstKind;
|
||||
use oxc_parser::Parser;
|
||||
use oxc_semantic::SemanticBuilder;
|
||||
use oxc_span::SourceType;
|
||||
|
||||
use crate::{
|
||||
LintOptions, ModuleRecord,
|
||||
context::{ContextHost, ContextSubHost, ContextSubHostOptions},
|
||||
};
|
||||
|
||||
let allocator = Allocator::default();
|
||||
let source = r#"exports["bar\uD800"] = ``; exports["foo"] = ``;"#;
|
||||
let parsed = Parser::new(&allocator, source, SourceType::default()).parse();
|
||||
assert!(parsed.diagnostics.is_empty());
|
||||
let semantic =
|
||||
SemanticBuilder::new_linter().build(allocator.alloc(parsed.program)).semantic;
|
||||
let ctx = Rc::new(ContextHost::new(
|
||||
"/test.snap",
|
||||
vec![ContextSubHost::new(
|
||||
semantic,
|
||||
Arc::new(ModuleRecord::default()),
|
||||
0,
|
||||
ContextSubHostOptions::default(),
|
||||
)],
|
||||
&allocator,
|
||||
LintOptions::default(),
|
||||
Arc::default(),
|
||||
))
|
||||
.spawn_for_test();
|
||||
|
||||
// A snapshot name with a lone surrogate matches no allowed pattern.
|
||||
for (allowed, foo_allowed, bar_allowed) in [
|
||||
(serde_json::json!([]), false, false),
|
||||
(serde_json::json!(["^foo"]), true, false),
|
||||
(serde_json::json!(["^bar.$"]), false, false),
|
||||
(serde_json::json!([".*"]), true, false),
|
||||
] {
|
||||
let config = NoLargeSnapshotsConfig::from_configuration(serde_json::json!([{
|
||||
"allowedSnapshots": { "/test.snap": allowed }
|
||||
}]))
|
||||
.unwrap();
|
||||
for node in ctx.nodes().iter() {
|
||||
if let AstKind::AssignmentExpression(assignment) = node.kind()
|
||||
&& let Some(member) = assignment.left.as_member_expression()
|
||||
{
|
||||
let is_foo = member.static_property_name().is_some_and(|name| name == "foo");
|
||||
let expected = if is_foo { foo_allowed } else { bar_allowed };
|
||||
assert_eq!(config.check_allowed_in_snapshots(member, &ctx), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_max_size_defaults_to_max_size() {
|
||||
let config = NoLargeSnapshotsConfig::from_configuration(serde_json::json!([{
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::path::PathBuf;
|
||||
use oxc_ast::{AstKind, ast::Argument};
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_span::Span;
|
||||
use oxc_str::JSStr;
|
||||
|
||||
use crate::context::LintContext;
|
||||
|
||||
@@ -65,12 +66,24 @@ pub fn run_once(ctx: &LintContext) {
|
||||
return;
|
||||
};
|
||||
|
||||
if string_literal.value.as_str().is_some_and(contains_mocks_dir) {
|
||||
if js_str_contains_mocks_dir(string_literal.value) {
|
||||
ctx.diagnostic(no_mocks_import_diagnostic(string_literal.span));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn js_str_contains_mocks_dir(value: JSStr<'_>) -> bool {
|
||||
if let Some(value) = value.as_str() {
|
||||
return contains_mocks_dir(value);
|
||||
}
|
||||
// Separators are ASCII, so splitting the WTF-8 bytes finds the same segments.
|
||||
// A segment with a lone surrogate never equals `__mocks__`.
|
||||
value
|
||||
.as_bytes()
|
||||
.split(|&byte| std::path::is_separator(char::from(byte)))
|
||||
.any(|segment| segment == b"__mocks__")
|
||||
}
|
||||
|
||||
fn contains_mocks_dir(value: &str) -> bool {
|
||||
PathBuf::from(value).components().any(|c| match c {
|
||||
std::path::Component::Normal(p) => p == std::ffi::OsStr::new("__mocks__"),
|
||||
|
||||
@@ -423,8 +423,7 @@ fn validate_title(
|
||||
return;
|
||||
}
|
||||
|
||||
// Regular expressions match UTF-8 only.
|
||||
// A title with a lone surrogate skips this check.
|
||||
// A title with a lone surrogate matches no disallowed word.
|
||||
if let Some(disallowed_words_reg) = &config.disallowed_words_reg
|
||||
&& let Some(utf8_title) = title.as_str()
|
||||
&& let Some(matched) = disallowed_words_reg.find(utf8_title)
|
||||
@@ -477,14 +476,11 @@ fn validate_title(
|
||||
return;
|
||||
};
|
||||
|
||||
// Regular expressions match UTF-8 only.
|
||||
// A title with a lone surrogate skips the pattern checks.
|
||||
let Some(title) = title.as_str() else {
|
||||
return;
|
||||
};
|
||||
// A title with a lone surrogate matches no configured pattern.
|
||||
let title = title.as_str();
|
||||
|
||||
if let Some((regex, message)) = config.must_match_patterns.get(&jest_fn_name)
|
||||
&& !regex.is_match(title)
|
||||
&& !title.is_some_and(|title| regex.is_match(title))
|
||||
{
|
||||
let raw_pattern = regex.as_str();
|
||||
let message = match message.as_ref() {
|
||||
@@ -495,7 +491,7 @@ fn validate_title(
|
||||
}
|
||||
|
||||
if let Some((regex, message)) = config.must_not_match_patterns.get(&jest_fn_name)
|
||||
&& regex.is_match(title)
|
||||
&& title.is_some_and(|title| regex.is_match(title))
|
||||
{
|
||||
let raw_pattern = regex.as_str();
|
||||
let message = match message.as_ref() {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use oxc_ast::{
|
||||
AstKind,
|
||||
ast::{Expression, TSEnumMember},
|
||||
@@ -27,12 +25,7 @@ fn no_duplicate_enum_values_diagnostic(
|
||||
let second_init_span = second_member.initializer.as_ref().map(GetSpan::span).unwrap();
|
||||
|
||||
OxcDiagnostic::warn(format!("Duplicate enum value `{value}`"))
|
||||
.with_help(format!(
|
||||
"Give {} a unique value",
|
||||
second_name
|
||||
.as_str()
|
||||
.map_or_else(|| Cow::Owned(format!("{second_name:?}")), Cow::Borrowed)
|
||||
))
|
||||
.with_help(format!("Give {} a unique value", second_name.display()))
|
||||
.with_labels([
|
||||
first_init_span.label(format!("{value} is first used as an initializer here")),
|
||||
second_init_span.label("and is re-used here"),
|
||||
|
||||
@@ -8,7 +8,7 @@ use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_semantic::IsGlobalReference;
|
||||
use oxc_span::Span;
|
||||
use oxc_str::static_ident;
|
||||
use oxc_str::{JSStr, static_ident};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
@@ -149,13 +149,17 @@ impl Rule for NoRequireImports {
|
||||
&& let Some(argument) = call_expr.arguments.first()
|
||||
{
|
||||
match argument {
|
||||
Argument::TemplateLiteral(template_literal) => {
|
||||
let Some(quasi) = template_literal.quasis.first() else {
|
||||
return;
|
||||
};
|
||||
if match_argument_value_with_regex(&self.allow, &quasi.value.raw) {
|
||||
return;
|
||||
}
|
||||
// Like typescript-eslint's `getStaticStringValue`, only a template
|
||||
// without expressions has a value, and it is the cooked one.
|
||||
Argument::TemplateLiteral(template_literal)
|
||||
if template_literal
|
||||
.single_quasi()
|
||||
.and_then(JSStr::as_str)
|
||||
.is_some_and(|value| {
|
||||
match_argument_value_with_regex(&self.allow, value)
|
||||
}) =>
|
||||
{
|
||||
return;
|
||||
}
|
||||
Argument::StringLiteral(string_literal)
|
||||
if string_literal.value.as_str().is_some_and(|value| {
|
||||
@@ -321,6 +325,24 @@ fn test() {
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
// A source with a lone surrogate matches no allow pattern.
|
||||
(
|
||||
r"const pkg = require('a\uD800b.json');",
|
||||
Some(serde_json::json!([{ "allow": ["\\.json$"] }])),
|
||||
),
|
||||
(
|
||||
r"import pkg = require('a\uD800b.json');",
|
||||
Some(serde_json::json!([{ "allow": ["\\.json$"] }])),
|
||||
),
|
||||
(
|
||||
r"const pkg = require(`a\uD800b.json`);",
|
||||
Some(serde_json::json!([{ "allow": ["^a.b\\.json$"] }])),
|
||||
),
|
||||
// A template with expressions has no static value.
|
||||
(
|
||||
"const pkg = require(`./${name}.json`);",
|
||||
Some(serde_json::json!([{ "allow": ["^\\./"] }])),
|
||||
),
|
||||
("var lib = require('lib');", None),
|
||||
("let lib2 = require('lib2');", None),
|
||||
(
|
||||
|
||||
@@ -74,13 +74,11 @@ impl Rule for NoConsoleSpaces {
|
||||
|
||||
for (i, arg) in call_expr.arguments.iter().enumerate() {
|
||||
if let Some(expression_arg) = arg.as_expression() {
|
||||
let (literal_raw, is_template_lit) = match expression_arg {
|
||||
let (has_leading_space, has_trailing_space, is_template_lit) = match expression_arg
|
||||
{
|
||||
Expression::StringLiteral(string_lit) => {
|
||||
let Some(literal_raw) = string_lit.value.as_str() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
(literal_raw, false)
|
||||
let value = string_lit.value;
|
||||
(value.starts_with(' '), value.ends_with(' '), false)
|
||||
}
|
||||
Expression::TemplateLiteral(string_lit) => {
|
||||
let literal_raw = string_lit
|
||||
@@ -89,13 +87,13 @@ impl Rule for NoConsoleSpaces {
|
||||
.trim_start_matches('`')
|
||||
.trim_end_matches('`');
|
||||
|
||||
(literal_raw, true)
|
||||
(literal_raw.starts_with(' '), literal_raw.ends_with(' '), true)
|
||||
}
|
||||
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if check_literal_leading(i, literal_raw) {
|
||||
if i != 0 && has_leading_space {
|
||||
report_diagnostic(
|
||||
"leading",
|
||||
// SAFETY: `is_method_call` ensures that `call_expr`'s `callee` is a `MemberExpression` with a `MemberExpression` as its `object`.
|
||||
@@ -106,7 +104,7 @@ impl Rule for NoConsoleSpaces {
|
||||
);
|
||||
}
|
||||
|
||||
if check_literal_trailing(i, literal_raw, call_expr_arg_len) {
|
||||
if i != call_expr_arg_len - 1 && has_trailing_space {
|
||||
report_diagnostic(
|
||||
"trailing",
|
||||
// SAFETY: `is_method_call` ensures that `call_expr`'s `callee` is a `MemberExpression` with a `MemberExpression` as its `object`.
|
||||
@@ -121,12 +119,6 @@ impl Rule for NoConsoleSpaces {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_literal_leading(i: usize, literal: &str) -> bool {
|
||||
i != 0 && literal.starts_with(' ')
|
||||
}
|
||||
fn check_literal_trailing(i: usize, literal: &str, call_expr_arg_len: usize) -> bool {
|
||||
i != call_expr_arg_len - 1 && literal.ends_with(' ')
|
||||
}
|
||||
fn report_diagnostic<'a>(
|
||||
direction: &'static str,
|
||||
ident: &'a str,
|
||||
@@ -195,6 +187,9 @@ fn test() {
|
||||
"lib.console.info(\" a \", \" b \");",
|
||||
"lib.console.warn(\" a \", \" b \");",
|
||||
"lib.console.error(\" a \", \" b \");",
|
||||
// A lone surrogate is not a space.
|
||||
r#"console.log("\uD800", "\uDC00");"#,
|
||||
r#"console.log("\uD800 x", "y \uDC00");"#,
|
||||
];
|
||||
|
||||
let fail = vec;",
|
||||
"console[\"warn\"](\" a \", \" b \");",
|
||||
"console[\"error\"](\" a \", \" b \");",
|
||||
// Lone surrogates elsewhere in the value do not hide the spaces.
|
||||
r#"console.log("\uD800 ", "def");"#,
|
||||
r#"console.log("abc", " \uDC00");"#,
|
||||
r#"console.log("_", " \uD83D\uDE00 ", "_");"#,
|
||||
];
|
||||
|
||||
let fix = vec![
|
||||
(r#"console.log("\uD800 ", bar)"#, r#"console.log("\uD800", bar)"#),
|
||||
(r#"console.log(foo, " \uDC00")"#, r#"console.log(foo, "\uDC00")"#),
|
||||
("console.log(\"foo \", bar)", "console.log(\"foo\", bar)"),
|
||||
("console.debug(\"foo \", bar)", "console.debug(\"foo\", bar)"),
|
||||
("console.info(\"foo \", bar)", "console.info(\"foo\", bar)"),
|
||||
|
||||
@@ -145,7 +145,13 @@ fn is_invalid_fetch_options<'a>(
|
||||
if let Some(Expression::StringLiteral(str_lit)) =
|
||||
&m.initializer
|
||||
{
|
||||
str_lit.value.as_str().map(CompactStr::from)
|
||||
// A value with a lone surrogate is not a known method.
|
||||
Some(CompactStr::from(
|
||||
str_lit
|
||||
.value
|
||||
.as_str()
|
||||
.unwrap_or(&UNKNOWN_METHOD_NAME),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -329,6 +335,8 @@ fn test() {
|
||||
(r#"new Request("/url", { method: getOptions().method, body: "some body" });"#),
|
||||
(r#"fetch("/url", { method: (options).method, body: "some body" });"#),
|
||||
(r#"new Request("/url", { method: (options).method, body: "some body" });"#),
|
||||
r#"enum M { A = "\uD800" } fetch(u, {method: M.A, body: "x"})"#,
|
||||
r#"enum M { A = "\uD800", B = "GET" } fetch(u, {method: M.A, body: "x"})"#,
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
|
||||
@@ -128,10 +128,12 @@ impl Rule for PreferDomNodeDataset {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(dataset_property_name) = string_lit.value.as_str().and_then(strip_data_prefix)
|
||||
else {
|
||||
let value = string_lit.value;
|
||||
if !value.starts_with("data-") && !value.starts_with("DATA-") {
|
||||
return;
|
||||
};
|
||||
}
|
||||
// The fix writes the rest of the name as text, so it is skipped for a lone surrogate.
|
||||
let dataset_property_name = value.as_str().and_then(strip_data_prefix);
|
||||
|
||||
match method {
|
||||
AttributeMethod::Set => {
|
||||
@@ -143,6 +145,9 @@ impl Rule for PreferDomNodeDataset {
|
||||
return fixer.noop();
|
||||
}
|
||||
|
||||
let Some(dataset_property_name) = dataset_property_name else {
|
||||
return fixer.noop();
|
||||
};
|
||||
let dataset_property_name_camel = dash_to_camel_case(dataset_property_name);
|
||||
let object_span = member_expr.object().span();
|
||||
|
||||
@@ -163,6 +168,9 @@ impl Rule for PreferDomNodeDataset {
|
||||
return fixer.noop();
|
||||
}
|
||||
|
||||
let Some(dataset_property_name) = dataset_property_name else {
|
||||
return fixer.noop();
|
||||
};
|
||||
let dataset_property_name_camel = dash_to_camel_case(dataset_property_name);
|
||||
let object_span = member_expr.object().span();
|
||||
|
||||
@@ -185,6 +193,9 @@ impl Rule for PreferDomNodeDataset {
|
||||
return fixer.noop();
|
||||
}
|
||||
|
||||
let Some(dataset_property_name) = dataset_property_name else {
|
||||
return fixer.noop();
|
||||
};
|
||||
let dataset_property_name_camel = dash_to_camel_case(dataset_property_name);
|
||||
let object_span = member_expr.object().span();
|
||||
|
||||
@@ -204,6 +215,9 @@ impl Rule for PreferDomNodeDataset {
|
||||
return fixer.noop();
|
||||
}
|
||||
|
||||
let Some(dataset_property_name) = dataset_property_name else {
|
||||
return fixer.noop();
|
||||
};
|
||||
let dataset_property_name_camel = dash_to_camel_case(dataset_property_name);
|
||||
let object_span = member_expr.object().span();
|
||||
|
||||
@@ -469,6 +483,7 @@ fn test() {
|
||||
r#"element.optional?.getAttribute("data-unicorn")?.length;"#,
|
||||
r#"element.getAttribute("data-unicorn").toString()"#,
|
||||
r#"(await promise).getAttribute("data-foo")"#,
|
||||
r#"el.setAttribute("data-\uD800", "x");"#,
|
||||
];
|
||||
|
||||
let fix = vec![
|
||||
@@ -611,6 +626,7 @@ fn test() {
|
||||
r"element.dataset.unicorn.toString()",
|
||||
),
|
||||
(r#"(await promise).getAttribute("data-foo")"#, r"(await promise).dataset.foo"),
|
||||
(r#"el.setAttribute("data-\uD800", "x");"#, r#"el.setAttribute("data-\uD800", "x");"#),
|
||||
];
|
||||
|
||||
Tester::new(PreferDomNodeDataset::NAME, PreferDomNodeDataset::PLUGIN, pass, fail)
|
||||
|
||||
@@ -9,7 +9,7 @@ use oxc_ast::{
|
||||
AstKind,
|
||||
ast::{
|
||||
BindingPattern, ExportFromDeclaration, ExportSpecifier, ImportAttributeKey,
|
||||
ImportDeclaration, ImportDeclarationSpecifier, ModuleExportName, Program, Statement,
|
||||
ImportDeclaration, ImportDeclarationSpecifier, ModuleExportName, Statement,
|
||||
VariableDeclarationKind, VariableDeclarator, WithClause, WithClauseKeyword,
|
||||
},
|
||||
};
|
||||
@@ -147,9 +147,7 @@ impl PreferExportFrom {
|
||||
|
||||
let re_export_decl = find_corresponding_export(ctx, import_decl);
|
||||
|
||||
let Some(source) = import_decl.source.value.as_str() else {
|
||||
return;
|
||||
};
|
||||
let source = ctx.source_range(import_decl.source.span);
|
||||
let with_clause = import_decl.with_clause.as_ref().map(|with_clause| {
|
||||
let keyword = match with_clause.keyword {
|
||||
WithClauseKeyword::With => "with",
|
||||
@@ -760,7 +758,7 @@ impl PreferExportFrom {
|
||||
}
|
||||
|
||||
fn format_export_statement(exports: &str, source: &str, with_clause: Option<&str>) -> String {
|
||||
let mut result = format!("export {{ {exports} }} from '{source}'");
|
||||
let mut result = format!("export {{ {exports} }} from {source}");
|
||||
|
||||
if let Some(clause) = with_clause {
|
||||
result.push(' ');
|
||||
@@ -782,7 +780,7 @@ impl PreferExportFrom {
|
||||
let formatted_name =
|
||||
if is_typescript_type { format!("type {name}") } else { name.to_string() };
|
||||
|
||||
let mut result = format!("export {formatted_name} from '{source}'");
|
||||
let mut result = format!("export {formatted_name} from {source}");
|
||||
|
||||
if let Some(clause) = with_clause {
|
||||
result.push(' ');
|
||||
@@ -1146,19 +1144,15 @@ fn find_corresponding_export<'a>(
|
||||
ctx: &LintContext<'a>,
|
||||
import_decl: &'a ImportDeclaration<'a>,
|
||||
) -> Option<&'a ExportFromDeclaration<'a>> {
|
||||
let source = import_decl.source.value.as_str()?;
|
||||
let program = ctx.nodes().program();
|
||||
|
||||
for requested_module in ctx.module_record().requested_modules.get(source)? {
|
||||
if requested_module.is_import {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(export_decl) =
|
||||
find_export_named_declaration_by_span(program, requested_module.statement_span)
|
||||
else {
|
||||
for statement in &program.body {
|
||||
let Statement::ExportFromDeclaration(export_decl) = statement else {
|
||||
continue;
|
||||
};
|
||||
if export_decl.source.value != import_decl.source.value {
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_matching_export_kind(import_decl, export_decl)
|
||||
&& has_matching_with_clause(
|
||||
@@ -1174,18 +1168,6 @@ fn find_corresponding_export<'a>(
|
||||
None
|
||||
}
|
||||
|
||||
fn find_export_named_declaration_by_span<'a>(
|
||||
program: &'a Program<'a>,
|
||||
span: Span,
|
||||
) -> Option<&'a ExportFromDeclaration<'a>> {
|
||||
program.body.iter().find_map(|statement| {
|
||||
let Statement::ExportFromDeclaration(export_decl) = statement else {
|
||||
return None;
|
||||
};
|
||||
(export_decl.span() == span).then_some(export_decl.as_ref())
|
||||
})
|
||||
}
|
||||
|
||||
fn is_matching_export_kind(
|
||||
import_decl: &ImportDeclaration<'_>,
|
||||
export_decl: &ExportFromDeclaration<'_>,
|
||||
@@ -1498,9 +1480,13 @@ fn test() {
|
||||
"import {foo} from './foo.json';
|
||||
export {foo};
|
||||
export {bar} from './foo.json' with { type: 'unknown' };",
|
||||
"import {a} from \"\\uD800\";\nexport {a};",
|
||||
"import {a} from \"\\uD800\";\nexport {a};\nexport {b} from \"\\uD800\";",
|
||||
];
|
||||
|
||||
let fix = vec![
|
||||
(r#"import {foo} from "foo'bar";export {foo};"#, "export { foo } from \"foo'bar\";\n"),
|
||||
(r"import {foo} from 'foo\\bar';export {foo};", "export { foo } from 'foo\\\\bar';\n"),
|
||||
// the `{` inside the comment is not the start of the specifier list; the
|
||||
// exported name used to be swallowed into the comment
|
||||
(
|
||||
@@ -1789,7 +1775,7 @@ fn test() {
|
||||
r#"import { foo } from "foo";
|
||||
export { foo };
|
||||
export type { bar } from "foo";"#,
|
||||
"export { foo } from 'foo';\n\n export type { bar } from \"foo\";",
|
||||
"export { foo } from \"foo\";\n\n export type { bar } from \"foo\";",
|
||||
),
|
||||
(
|
||||
r#"import { foo } from "foo";
|
||||
@@ -1864,7 +1850,7 @@ fn test() {
|
||||
(
|
||||
r#"import type foo from "foo";
|
||||
export default foo"#,
|
||||
"export { type default } from 'foo';\n",
|
||||
"export { type default } from \"foo\";\n",
|
||||
),
|
||||
(
|
||||
"import {type foo} from 'foo';
|
||||
@@ -1952,6 +1938,11 @@ fn test() {
|
||||
export {bar,} from 'foo';",
|
||||
"export {bar, default as foo,} from 'foo';",
|
||||
),
|
||||
("import {a} from \"\\uD800\";\nexport {a};", "export { a } from \"\\uD800\";\n"),
|
||||
(
|
||||
"import {a} from \"\\uD800\";\nexport {a};\nexport {b} from \"\\uD800\";",
|
||||
"export {b, a} from \"\\uD800\";",
|
||||
),
|
||||
];
|
||||
Tester::new(PreferExportFrom::NAME, PreferExportFrom::PLUGIN, pass, fail)
|
||||
.expect_fix(fix)
|
||||
|
||||
@@ -285,10 +285,10 @@ fn is_ternary_expression(expression: &Expression<'_>) -> bool {
|
||||
matches!(expression.get_inner_expression(), Expression::ConditionalExpression(_))
|
||||
}
|
||||
|
||||
fn is_same_assignment_target(
|
||||
left: &AssignmentTarget<'_>,
|
||||
right: &AssignmentTarget<'_>,
|
||||
ctx: &LintContext<'_>,
|
||||
fn is_same_assignment_target<'a>(
|
||||
left: &AssignmentTarget<'a>,
|
||||
right: &AssignmentTarget<'a>,
|
||||
ctx: &LintContext<'a>,
|
||||
) -> bool {
|
||||
if let (
|
||||
AssignmentTarget::AssignmentTargetIdentifier(left),
|
||||
@@ -301,9 +301,10 @@ fn is_same_assignment_target(
|
||||
if let (Some(left_member), Some(right_member)) =
|
||||
(left.as_member_expression(), right.as_member_expression())
|
||||
{
|
||||
if let (Some(left_name), Some(right_name)) =
|
||||
(member_static_property_name(left_member), member_static_property_name(right_member))
|
||||
{
|
||||
if let (Some(left_name), Some(right_name)) = (
|
||||
member_static_property_name(left_member, ctx),
|
||||
member_static_property_name(right_member, ctx),
|
||||
) {
|
||||
return left_name == right_name
|
||||
&& is_same_expression(
|
||||
left_member.object().get_inner_expression(),
|
||||
@@ -321,16 +322,19 @@ fn is_same_assignment_target(
|
||||
}
|
||||
}
|
||||
|
||||
fn member_static_property_name(member: &MemberExpression<'_>) -> Option<String> {
|
||||
if let Some(name) = member.static_property_name().and_then(JSStr::as_str) {
|
||||
return Some(name.to_string());
|
||||
fn member_static_property_name<'a>(
|
||||
member: &MemberExpression<'a>,
|
||||
ctx: &LintContext<'a>,
|
||||
) -> Option<JSStr<'a>> {
|
||||
if let Some(name) = member.static_property_name() {
|
||||
return Some(name);
|
||||
}
|
||||
|
||||
let MemberExpression::ComputedMemberExpression(computed) = member else {
|
||||
return None;
|
||||
};
|
||||
|
||||
static_string_value(computed.expression.get_inner_expression())
|
||||
static_string_value(computed.expression.get_inner_expression(), ctx.allocator())
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -349,6 +353,9 @@ fn test() {
|
||||
}",
|
||||
None,
|
||||
),
|
||||
// Different lone surrogates are different names.
|
||||
(r#"if (test) { obj["\uD800"] = a; } else { obj["\uDC00"] = b; }"#, None),
|
||||
(r#"if (test) { obj["\uD83D\uDE00"] = a; } else { obj["\uD83D"] = b; }"#, None),
|
||||
(
|
||||
"function unicorn() {
|
||||
if(test){
|
||||
@@ -607,6 +614,10 @@ fn test() {
|
||||
}",
|
||||
None,
|
||||
),
|
||||
// Computed member names are compared by their JavaScript value.
|
||||
(r#"if (test) { obj["\uD800"] = a; } else { obj["\uD800"] = b; }"#, None),
|
||||
(r#"if (test) { obj["\uD800" + "x"] = a; } else { obj[`\uD800x`] = b; }"#, None),
|
||||
(r#"if (test) { obj["\uD83D" + "\uDE00"] = a; } else { obj["\uD83D\uDE00"] = b; }"#, None),
|
||||
(
|
||||
"async function unicorn() {
|
||||
if(test){
|
||||
|
||||
@@ -229,6 +229,11 @@ fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
// A URL or base with a lone surrogate is not checked.
|
||||
(r#"new URL("./a\uD800", "https://x.com/")"#, None),
|
||||
(r#"new URL("a\uD800", "https://x.com/")"#, Some(serde_json::json!(["always"]))),
|
||||
(r#"new URL("./foo", "https://x.com/\uD800/")"#, None),
|
||||
(r#"new URL("foo", "https://x.com/\uD800/")"#, Some(serde_json::json!(["always"]))),
|
||||
// Default "never" option
|
||||
(r#"URL("./foo", base)"#, None),
|
||||
(r#"new URL(...["./foo"], base)"#, None),
|
||||
|
||||
@@ -27,6 +27,9 @@ fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let mut pass = vec![
|
||||
// A segment with a lone surrogate is not `__mocks__`.
|
||||
(r"require('./\uD800__mocks__/x')", None),
|
||||
(r"require('./__mocks__\uDC00/x')", None),
|
||||
("import something from 'something'", None),
|
||||
("require('somethingElse')", None),
|
||||
("require('./__mocks__.js')", None),
|
||||
@@ -40,6 +43,9 @@ fn test() {
|
||||
];
|
||||
|
||||
let mut fail = vec![
|
||||
(r"require('./\uD83D\uDE00/__mocks__')", None),
|
||||
(r"require('./__mocks__/\uD800')", None),
|
||||
(r"require('\uDC00/__mocks__/x')", None),
|
||||
("require('./__mocks__')", None),
|
||||
("require('./__mocks__/')", None),
|
||||
("require('./__mocks__/index')", None),
|
||||
|
||||
@@ -2,6 +2,7 @@ use oxc_ast::{AstKind, ast::Argument};
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::Span;
|
||||
use oxc_str::JSStr;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
@@ -11,10 +12,16 @@ use crate::{
|
||||
utils::{PossibleJestNode, parse_general_jest_fn_call},
|
||||
};
|
||||
|
||||
fn prefer_import_in_mock_diagnostic(span: Span, path: &str) -> OxcDiagnostic {
|
||||
let help = format!(
|
||||
"Dynamic import improves the type information and IntelliSense. Substitute `{path}` with `import('{path}')`"
|
||||
);
|
||||
fn prefer_import_in_mock_diagnostic(span: Span, path: JSStr) -> OxcDiagnostic {
|
||||
// Debug supplies a quoted, escaped spelling when the path has a lone surrogate.
|
||||
let help = match path.as_str() {
|
||||
Some(path) => format!(
|
||||
"Dynamic import improves the type information and IntelliSense. Substitute `{path}` with `import('{path}')`"
|
||||
),
|
||||
None => format!(
|
||||
"Dynamic import improves the type information and IntelliSense. Substitute `{path:?}` with `import({path:?})`"
|
||||
),
|
||||
};
|
||||
|
||||
OxcDiagnostic::warn("Mocked modules must be dynamic imported.").with_help(help).with_label(span)
|
||||
}
|
||||
@@ -118,13 +125,18 @@ impl PreferImportInMock {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(value) = import_value.value.as_str() else { return };
|
||||
let value = import_value.value;
|
||||
ctx.diagnostic_with_fix(
|
||||
prefer_import_in_mock_diagnostic(call_expr.arguments_span().unwrap(), value),
|
||||
|fixer| {
|
||||
if !self.fixable {
|
||||
return fixer.noop();
|
||||
}
|
||||
// The fix is built from `&str`, so a path with a lone surrogate is
|
||||
// reported without it.
|
||||
let Some(value) = value.as_str() else {
|
||||
return fixer.noop();
|
||||
};
|
||||
|
||||
fixer.replace(import_value.span, format!("import('{value}')"))
|
||||
},
|
||||
@@ -151,6 +163,10 @@ fn test() {
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
// Lone surrogates are part of the value.
|
||||
// A search or prefix check must still see the rest.
|
||||
(r#"vi.mock("\uD800")"#, None),
|
||||
(r#"vi.doMock("a\uDC00b", () => ({}))"#, None),
|
||||
("vi.mock('foo', () => {})", Some(serde_json::json!([ { "fixable": false, }, ]))),
|
||||
(r#"vi.mock("node:fs/promises")"#, Some(serde_json::json!([ { "fixable": false, }, ]))),
|
||||
(
|
||||
@@ -190,6 +206,8 @@ fn test() {
|
||||
];
|
||||
|
||||
let fix = vec![
|
||||
// A path with a lone surrogate gets no fix.
|
||||
(r#"vi.mock("\uD800")"#, r#"vi.mock("\uD800")"#),
|
||||
("vi.mock('foo', () => {})", "vi.mock(import('foo'), () => {})"),
|
||||
(r#"vi.mock("node:fs/promises")"#, "vi.mock(import('node:fs/promises'))"),
|
||||
(
|
||||
|
||||
@@ -8,6 +8,7 @@ use oxc_ast::{
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::{GetSpan, Span};
|
||||
use oxc_str::JSStr;
|
||||
|
||||
use crate::{
|
||||
AstNode,
|
||||
@@ -19,7 +20,7 @@ use crate::{
|
||||
|
||||
fn component_definition_name_casing_diagnostic(
|
||||
span: Span,
|
||||
value: &str,
|
||||
value: impl std::fmt::Display,
|
||||
case_type: &str,
|
||||
) -> OxcDiagnostic {
|
||||
OxcDiagnostic::warn(format!("Property name \"{value}\" is not {case_type}.")).with_label(span)
|
||||
@@ -143,21 +144,26 @@ impl ComponentDefinitionNameCasing {
|
||||
self.check_name_node(&prop.value, ctx);
|
||||
}
|
||||
|
||||
fn check_name_node(&self, expr: &Expression<'_>, ctx: &LintContext<'_>) {
|
||||
fn check_name_node<'a>(&self, expr: &Expression<'a>, ctx: &LintContext<'a>) {
|
||||
let inner = expr.get_inner_expression();
|
||||
let Some((value, inner_span)) = extract_convertible(inner) else { return };
|
||||
|
||||
let case_type = self.0;
|
||||
if check_case(&value, case_type) {
|
||||
if check_case(value, case_type) {
|
||||
return;
|
||||
}
|
||||
|
||||
let report_span = inner.span();
|
||||
let case_type_str = case_type.as_str();
|
||||
let diagnostic =
|
||||
component_definition_name_casing_diagnostic(report_span, value.as_str(), case_type_str);
|
||||
let diagnostic = component_definition_name_casing_diagnostic(
|
||||
report_span,
|
||||
value.display(),
|
||||
case_type_str,
|
||||
);
|
||||
|
||||
if let Some(converted) = exact_convert(&value, case_type) {
|
||||
// The case converters take UTF-8 text.
|
||||
// A name with a lone surrogate gets no fix.
|
||||
if let Some(converted) = value.as_str().and_then(|value| exact_convert(value, case_type)) {
|
||||
ctx.diagnostic_with_fix(diagnostic, |fixer| fixer.replace(inner_span, converted));
|
||||
} else {
|
||||
ctx.diagnostic(diagnostic);
|
||||
@@ -169,26 +175,26 @@ impl ComponentDefinitionNameCasing {
|
||||
/// simple template literal (no expressions, single quasi). `inner_span`
|
||||
/// is the range of the literal *contents* (excluding the quotes / backticks),
|
||||
/// suitable as a fix target.
|
||||
fn extract_convertible(expr: &Expression<'_>) -> Option<(String, Span)> {
|
||||
fn extract_convertible<'a>(expr: &Expression<'a>) -> Option<(JSStr<'a>, Span)> {
|
||||
match expr {
|
||||
Expression::StringLiteral(lit) => {
|
||||
let inner = Span::new(lit.span.start + 1, lit.span.end - 1);
|
||||
Some((lit.value.as_str()?.to_owned(), inner))
|
||||
Some((lit.value, inner))
|
||||
}
|
||||
Expression::TemplateLiteral(tpl) => {
|
||||
if !tpl.expressions.is_empty() || tpl.quasis.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
let quasi = tpl.quasis.first()?;
|
||||
let cooked = quasi.value.cooked.as_ref()?;
|
||||
let cooked = quasi.value.cooked?;
|
||||
let inner = Span::new(tpl.span.start + 1, tpl.span.end - 1);
|
||||
Some((cooked.as_str()?.to_owned(), inner))
|
||||
Some((cooked, inner))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn check_case(s: &str, case_type: CaseType) -> bool {
|
||||
fn check_case(s: JSStr<'_>, case_type: CaseType) -> bool {
|
||||
match case_type {
|
||||
CaseType::PascalCase => vue_casing::is_pascal_case(s),
|
||||
CaseType::KebabCase => vue_casing::is_kebab_case(s),
|
||||
@@ -205,7 +211,7 @@ fn exact_convert(s: &str, case_type: CaseType) -> Option<String> {
|
||||
CaseType::PascalCase => vue_casing::pascal_case(s),
|
||||
CaseType::KebabCase => vue_casing::kebab_case(s),
|
||||
};
|
||||
if check_case(&converted, case_type) { Some(converted) } else { None }
|
||||
if check_case(JSStr::from(converted.as_str()), case_type) { Some(converted) } else { None }
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -226,6 +232,36 @@ fn test() {
|
||||
None,
|
||||
vue(),
|
||||
),
|
||||
(
|
||||
r#"<script>
|
||||
export default {
|
||||
name: "Foo\uD800"
|
||||
}
|
||||
</script>"#,
|
||||
None,
|
||||
None,
|
||||
vue(),
|
||||
),
|
||||
(
|
||||
r"<script>
|
||||
export default {
|
||||
name: `\uDC00Foo`
|
||||
}
|
||||
</script>",
|
||||
None,
|
||||
None,
|
||||
vue(),
|
||||
),
|
||||
(
|
||||
r#"<script>
|
||||
export default {
|
||||
name: "foo-\uD83D\uDE00"
|
||||
}
|
||||
</script>"#,
|
||||
Some(serde_json::json!(["kebab-case"])),
|
||||
None,
|
||||
vue(),
|
||||
),
|
||||
(
|
||||
"<script>
|
||||
export default {
|
||||
@@ -346,6 +382,38 @@ fn test() {
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
// A lone surrogate is neither a letter nor a separator, so the surrounding casing decides.
|
||||
// The fix is declined because the converters need UTF-8.
|
||||
(
|
||||
r#"<script>
|
||||
export default {
|
||||
name: "foo-\uD800"
|
||||
}
|
||||
</script>"#,
|
||||
None,
|
||||
None,
|
||||
vue(),
|
||||
),
|
||||
(
|
||||
r"<script>
|
||||
export default {
|
||||
name: `\uDC00_bar`
|
||||
}
|
||||
</script>",
|
||||
None,
|
||||
None,
|
||||
vue(),
|
||||
),
|
||||
(
|
||||
r#"<script>
|
||||
export default {
|
||||
name: "Foo\uD83D\uDE00"
|
||||
}
|
||||
</script>"#,
|
||||
Some(serde_json::json!(["kebab-case"])),
|
||||
None,
|
||||
vue(),
|
||||
),
|
||||
(
|
||||
"<script>
|
||||
export default {
|
||||
|
||||
@@ -53,7 +53,11 @@ fn reserved_key_diagnostic(name: &str, span: Span) -> OxcDiagnostic {
|
||||
OxcDiagnostic::warn(format!("Key `{name}` is reserved.")).with_label(span)
|
||||
}
|
||||
|
||||
fn starts_with_underscore_diagnostic(key: &str, group: &str, span: Span) -> OxcDiagnostic {
|
||||
fn starts_with_underscore_diagnostic(
|
||||
key: impl std::fmt::Display,
|
||||
group: &str,
|
||||
span: Span,
|
||||
) -> OxcDiagnostic {
|
||||
OxcDiagnostic::warn(format!("Key `{key}` is reserved in `{group}` group.")).with_label(span)
|
||||
}
|
||||
|
||||
@@ -220,14 +224,17 @@ impl NoReservedKeys {
|
||||
fn check_keys<'a>(&self, group: &str, obj: &ObjectExpression<'a>, ctx: &LintContext<'a>) {
|
||||
for prop_kind in &obj.properties {
|
||||
let ObjectPropertyKind::ObjectProperty(p) = prop_kind else { continue };
|
||||
let Some(n) = p.key.static_name().and_then(StaticPropertyName::into_cow_str) else {
|
||||
let Some(name) = p.key.static_name() else {
|
||||
continue;
|
||||
};
|
||||
let name = name.as_js_str();
|
||||
let span = p.key.span();
|
||||
if self.is_reserved(&n) {
|
||||
ctx.diagnostic(reserved_key_diagnostic(&n, span));
|
||||
} else if matches!(group, "data" | "asyncData") && n.starts_with('_') {
|
||||
ctx.diagnostic(starts_with_underscore_diagnostic(&n, group, span));
|
||||
if let Some(n) = name.as_str()
|
||||
&& self.is_reserved(n)
|
||||
{
|
||||
ctx.diagnostic(reserved_key_diagnostic(n, span));
|
||||
} else if matches!(group, "data" | "asyncData") && name.starts_with('_') {
|
||||
ctx.diagnostic(starts_with_underscore_diagnostic(name.display(), group, span));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -585,6 +592,18 @@ fn test() {
|
||||
None,
|
||||
Some(PathBuf::from("test.vue")),
|
||||
),
|
||||
(
|
||||
r#"
|
||||
<script>
|
||||
new Vue({
|
||||
data: () => ({ "_\uD800": 1 })
|
||||
})
|
||||
</script>
|
||||
"#,
|
||||
None,
|
||||
None,
|
||||
Some(PathBuf::from("test.vue")),
|
||||
),
|
||||
// `reserved` + `groups` options (eslint-plugin-vue parity)
|
||||
(
|
||||
"
|
||||
|
||||
@@ -3,7 +3,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use oxc_ast::{
|
||||
AstKind, StaticPropertyName,
|
||||
AstKind,
|
||||
ast::{
|
||||
ArrayExpression, ArrayExpressionElement, CallExpression, Expression, ObjectExpression,
|
||||
ObjectPropertyKind, PropertyKey, TSSignature,
|
||||
@@ -12,6 +12,7 @@ use oxc_ast::{
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::{GetSpan, Span};
|
||||
use oxc_str::JSStr;
|
||||
|
||||
use crate::{
|
||||
AstNode,
|
||||
@@ -24,7 +25,11 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
fn prop_name_casing_diagnostic(span: Span, name: &str, case_type: &str) -> OxcDiagnostic {
|
||||
fn prop_name_casing_diagnostic(
|
||||
span: Span,
|
||||
name: impl std::fmt::Display,
|
||||
case_type: &str,
|
||||
) -> OxcDiagnostic {
|
||||
OxcDiagnostic::warn(format!("Prop '{name}' is not in {case_type}.")).with_label(span)
|
||||
}
|
||||
|
||||
@@ -162,9 +167,7 @@ impl PropNameCasing {
|
||||
fn check_array_props<'a>(&self, arr: &ArrayExpression<'a>, ctx: &LintContext<'a>) {
|
||||
for element in &arr.elements {
|
||||
let ArrayExpressionElement::StringLiteral(lit) = element else { continue };
|
||||
if let Some(value) = lit.value.as_str() {
|
||||
self.report_if_invalid(value, lit.span, ctx);
|
||||
}
|
||||
self.report_if_invalid(lit.value, lit.span, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +175,7 @@ impl PropNameCasing {
|
||||
for prop in &obj.properties {
|
||||
let ObjectPropertyKind::ObjectProperty(prop) = prop else { continue };
|
||||
let Some((name, span)) = property_key_static_name(&prop.key) else { continue };
|
||||
self.report_if_invalid(name.as_ref(), span, ctx);
|
||||
self.report_if_invalid(name, span, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,31 +185,29 @@ impl PropNameCasing {
|
||||
TSSignature::TSMethodSignature(sig) => (sig.key.static_name(), sig.key.span()),
|
||||
_ => return,
|
||||
};
|
||||
let name = key_opt.and_then(StaticPropertyName::into_cow_str);
|
||||
let Some(name) = name else { return };
|
||||
self.report_if_invalid(&name, span, ctx);
|
||||
let Some(name) = key_opt else { return };
|
||||
self.report_if_invalid(name.as_js_str(), span, ctx);
|
||||
}
|
||||
|
||||
fn report_if_invalid(&self, name: &str, span: Span, ctx: &LintContext<'_>) {
|
||||
fn report_if_invalid(&self, name: JSStr<'_>, span: Span, ctx: &LintContext<'_>) {
|
||||
let Config(case_type, options) = &*self.0;
|
||||
if is_ignored(name, &options.ignore_props) {
|
||||
// A name with a lone surrogate matches no `ignoreProps` pattern.
|
||||
if name.as_str().is_some_and(|name| is_ignored(name, &options.ignore_props)) {
|
||||
return;
|
||||
}
|
||||
if check_case(name, *case_type) {
|
||||
return;
|
||||
}
|
||||
ctx.diagnostic(prop_name_casing_diagnostic(span, name, case_type.as_str()));
|
||||
ctx.diagnostic(prop_name_casing_diagnostic(span, name.display(), case_type.as_str()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `(static_name, span_of_key_text)` for a property key when it can be
|
||||
/// resolved statically. Dynamic keys (computed identifiers, calls, binary
|
||||
/// expressions, etc.) return `None`.
|
||||
fn property_key_static_name<'a>(
|
||||
key: &PropertyKey<'a>,
|
||||
) -> Option<(std::borrow::Cow<'a, str>, Span)> {
|
||||
fn property_key_static_name<'a>(key: &PropertyKey<'a>) -> Option<(JSStr<'a>, Span)> {
|
||||
match key {
|
||||
PropertyKey::StaticIdentifier(ident) => Some((ident.name.as_str().into(), ident.span)),
|
||||
PropertyKey::StaticIdentifier(ident) => Some((ident.name.into(), ident.span)),
|
||||
PropertyKey::PrivateIdentifier(_) => None,
|
||||
key => {
|
||||
// Computed key expressions: pull the inner expression and accept
|
||||
@@ -215,24 +216,22 @@ fn property_key_static_name<'a>(
|
||||
// unresolvable and skipped.
|
||||
let expr = key.as_expression()?.get_inner_expression();
|
||||
match expr {
|
||||
Expression::StringLiteral(lit) => Some((lit.value.as_str()?.into(), lit.span)),
|
||||
Expression::StringLiteral(lit) => Some((lit.value, lit.span)),
|
||||
Expression::TemplateLiteral(tpl)
|
||||
if tpl.expressions.is_empty() && tpl.quasis.len() == 1 =>
|
||||
{
|
||||
let quasi = tpl.quasis.first()?;
|
||||
let cooked = quasi.value.cooked.as_ref()?;
|
||||
Some((cooked.as_str()?.into(), tpl.span))
|
||||
}
|
||||
Expression::RegExpLiteral(regex) => {
|
||||
Some((regex.raw.as_ref()?.as_str().into(), regex.span))
|
||||
let cooked = quasi.value.cooked?;
|
||||
Some((cooked, tpl.span))
|
||||
}
|
||||
Expression::RegExpLiteral(regex) => Some((regex.raw?.into(), regex.span)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_case(s: &str, case_type: CaseType) -> bool {
|
||||
fn check_case(s: JSStr<'_>, case_type: CaseType) -> bool {
|
||||
match case_type {
|
||||
CaseType::CamelCase => vue_casing::is_camel_case(s),
|
||||
CaseType::SnakeCase => vue_casing::is_snake_case(s),
|
||||
@@ -262,6 +261,42 @@ fn test() {
|
||||
None,
|
||||
Some(PathBuf::from("test.vue")),
|
||||
), // languageOptions,
|
||||
(
|
||||
r#"
|
||||
<script>
|
||||
export default {
|
||||
props: ["greeting\uD800"]
|
||||
}
|
||||
</script>
|
||||
"#,
|
||||
None,
|
||||
None,
|
||||
Some(PathBuf::from("test.vue")),
|
||||
),
|
||||
(
|
||||
r#"
|
||||
<script>
|
||||
export default {
|
||||
props: { "\uDC00text": String, [`greeting\uD83D\uDE00`]: String }
|
||||
}
|
||||
</script>
|
||||
"#,
|
||||
None,
|
||||
None,
|
||||
Some(PathBuf::from("test.vue")),
|
||||
),
|
||||
(
|
||||
r#"
|
||||
<script>
|
||||
export default {
|
||||
props: ["greeting_\uD800"]
|
||||
}
|
||||
</script>
|
||||
"#,
|
||||
Some(serde_json::json!(["snake_case"])),
|
||||
None,
|
||||
Some(PathBuf::from("test.vue")),
|
||||
),
|
||||
(
|
||||
"
|
||||
<script>
|
||||
@@ -666,6 +701,58 @@ fn test() {
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
// A name with a lone surrogate matches no `ignoreProps` pattern.
|
||||
(
|
||||
r#"
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
"\uDC00_text": String,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
"#,
|
||||
Some(serde_json::json!([ "camelCase", { "ignoreProps": ["_text$"] } ])),
|
||||
None,
|
||||
Some(PathBuf::from("test.vue")),
|
||||
),
|
||||
// A lone surrogate is neither a letter nor a separator, so the surrounding casing decides.
|
||||
(
|
||||
r#"
|
||||
<script>
|
||||
export default {
|
||||
props: ["greeting-\uD800"]
|
||||
}
|
||||
</script>
|
||||
"#,
|
||||
None,
|
||||
None,
|
||||
Some(PathBuf::from("test.vue")),
|
||||
),
|
||||
(
|
||||
r#"
|
||||
<script>
|
||||
export default {
|
||||
props: { "\uDC00_text": String }
|
||||
}
|
||||
</script>
|
||||
"#,
|
||||
None,
|
||||
None,
|
||||
Some(PathBuf::from("test.vue")),
|
||||
),
|
||||
(
|
||||
r"
|
||||
<script>
|
||||
export default {
|
||||
props: { [`Greeting\uD83D\uDE00`]: String }
|
||||
}
|
||||
</script>
|
||||
",
|
||||
None,
|
||||
None,
|
||||
Some(PathBuf::from("test.vue")),
|
||||
),
|
||||
(
|
||||
"
|
||||
<script>
|
||||
|
||||
@@ -275,6 +275,13 @@ source: crates/oxc_linter/src/tester.rs
|
||||
╰────
|
||||
help: Rename the function or the variable/property so the names satisfy this rule.
|
||||
|
||||
⚠ eslint(func-name-matching): Function name `foo` should match property name `\ud800`.
|
||||
╭─[func_name_matching.tsx:1:54]
|
||||
1 │ Object.defineProperty(o, "\uD800", { value: function foo() {} })
|
||||
· ───
|
||||
╰────
|
||||
help: Rename the function or the variable/property so the names satisfy this rule.
|
||||
|
||||
⚠ eslint(func-name-matching): Function name `y` should match property name `x`.
|
||||
╭─[func_name_matching.tsx:1:24]
|
||||
1 │ class C { x = function y() {}; }
|
||||
|
||||
@@ -2,6 +2,86 @@
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ eslint(new-cap): A constructor name should not start with a lowercase letter.
|
||||
╭─[new_cap.tsx:1:7]
|
||||
1 │ new o['a\uD800']();
|
||||
· ────┬────
|
||||
· ╰── This should be uppercase
|
||||
╰────
|
||||
help: Capitalize the first letter of the constructor name, or add it to the exceptions list if it should not be capitalized.
|
||||
|
||||
⚠ eslint(new-cap): A function with a name starting with an uppercase letter should only be used as a constructor.
|
||||
╭─[new_cap.tsx:1:3]
|
||||
1 │ o['A\uDC00']();
|
||||
· ────┬────
|
||||
· ╰── This should be called with `new`
|
||||
╰────
|
||||
help: Use the new operator when calling this function, or add it to the exceptions list if it should not be called with new.
|
||||
|
||||
⚠ eslint(new-cap): A constructor name should not start with a lowercase letter.
|
||||
╭─[new_cap.tsx:1:7]
|
||||
1 │ new o['a\uD800']();
|
||||
· ────┬────
|
||||
· ╰── This should be uppercase
|
||||
╰────
|
||||
help: Capitalize the first letter of the constructor name, or add it to the exceptions list if it should not be capitalized.
|
||||
|
||||
⚠ eslint(new-cap): A constructor name should not start with a lowercase letter.
|
||||
╭─[new_cap.tsx:1:7]
|
||||
1 │ new o['a\uD800']();
|
||||
· ────┬────
|
||||
· ╰── This should be uppercase
|
||||
╰────
|
||||
help: Capitalize the first letter of the constructor name, or add it to the exceptions list if it should not be capitalized.
|
||||
|
||||
⚠ eslint(new-cap): A function with a name starting with an uppercase letter should only be used as a constructor.
|
||||
╭─[new_cap.tsx:1:3]
|
||||
1 │ o['A\uDC00']();
|
||||
· ────┬────
|
||||
· ╰── This should be called with `new`
|
||||
╰────
|
||||
help: Use the new operator when calling this function, or add it to the exceptions list if it should not be called with new.
|
||||
|
||||
⚠ eslint(new-cap): A function with a name starting with an uppercase letter should only be used as a constructor.
|
||||
╭─[new_cap.tsx:1:6]
|
||||
1 │ (o?.['A\uDC00'])();
|
||||
· ────┬────
|
||||
· ╰── This should be called with `new`
|
||||
╰────
|
||||
help: Use the new operator when calling this function, or add it to the exceptions list if it should not be called with new.
|
||||
|
||||
⚠ eslint(new-cap): A function with a name starting with an uppercase letter should only be used as a constructor.
|
||||
╭─[new_cap.tsx:1:19]
|
||||
1 │ services.registry[`\u0043reateDocumentForCurrentSession`]();
|
||||
· ───────────────────┬──────────────────
|
||||
· ╰── This should be called with `new`
|
||||
╰────
|
||||
help: Use the new operator when calling this function, or add it to the exceptions list if it should not be called with new.
|
||||
|
||||
⚠ eslint(new-cap): A constructor name should not start with a lowercase letter.
|
||||
╭─[new_cap.tsx:1:7]
|
||||
1 │ new o[`a\uD800`]();
|
||||
· ────┬────
|
||||
· ╰── This should be uppercase
|
||||
╰────
|
||||
help: Capitalize the first letter of the constructor name, or add it to the exceptions list if it should not be capitalized.
|
||||
|
||||
⚠ eslint(new-cap): A constructor name should not start with a lowercase letter.
|
||||
╭─[new_cap.tsx:1:15]
|
||||
1 │ var x = new o["a\uD800"]();
|
||||
· ────┬────
|
||||
· ╰── This should be uppercase
|
||||
╰────
|
||||
help: Capitalize the first letter of the constructor name, or add it to the exceptions list if it should not be capitalized.
|
||||
|
||||
⚠ eslint(new-cap): A function with a name starting with an uppercase letter should only be used as a constructor.
|
||||
╭─[new_cap.tsx:1:11]
|
||||
1 │ var x = o["A\uDC00"]();
|
||||
· ────┬────
|
||||
· ╰── This should be called with `new`
|
||||
╰────
|
||||
help: Use the new operator when calling this function, or add it to the exceptions list if it should not be called with new.
|
||||
|
||||
⚠ eslint(new-cap): A constructor name should not start with a lowercase letter.
|
||||
╭─[new_cap.tsx:1:13]
|
||||
1 │ var x = new c();
|
||||
|
||||
@@ -2,6 +2,20 @@
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ eslint(prefer-named-capture-group): Capture group should be named.
|
||||
╭─[prefer_named_capture_group.tsx:1:12]
|
||||
1 │ new RegExp("(a)[" + "\uD800" + "-" + "\uE000" + "]");
|
||||
· ────────────────────────────────────────
|
||||
╰────
|
||||
help: Use a named capture group like "(?<name>...)" — this regex has 1 unnamed group.
|
||||
|
||||
⚠ eslint(prefer-named-capture-group): Capture group should be named.
|
||||
╭─[prefer_named_capture_group.tsx:1:12]
|
||||
1 │ new RegExp("(a)[" + "\uD800" + "-" + "\uE000" + "]", "u");
|
||||
· ────────────────────────────────────────
|
||||
╰────
|
||||
help: Use a named capture group like "(?<name>...)" — this regex has 1 unnamed group.
|
||||
|
||||
⚠ eslint(prefer-named-capture-group): Capture group should be named.
|
||||
╭─[prefer_named_capture_group.tsx:1:2]
|
||||
1 │ /([0-9]{4})/
|
||||
@@ -142,6 +156,13 @@ source: crates/oxc_linter/src/tester.rs
|
||||
╰────
|
||||
help: Use a named capture group like "(?<name>...)" — this regex has 1 unnamed group.
|
||||
|
||||
⚠ eslint(prefer-named-capture-group): Capture group should be named.
|
||||
╭─[prefer_named_capture_group.tsx:1:12]
|
||||
1 │ new RegExp("(a)" + "\uD800");
|
||||
· ────────────────
|
||||
╰────
|
||||
help: Use a named capture group like "(?<name>...)" — this regex has 1 unnamed group.
|
||||
|
||||
⚠ eslint(prefer-named-capture-group): Capture group should be named.
|
||||
╭─[prefer_named_capture_group.tsx:1:8]
|
||||
1 │ RegExp('(a)'+'')
|
||||
|
||||
@@ -2,6 +2,22 @@
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ eslint(require-unicode-regexp): Use the 'u' flag.
|
||||
╭─[require_unicode_regexp.tsx:1:1]
|
||||
1 │ new RegExp("foo", "\uD800")
|
||||
· ───────────────────────────
|
||||
╰────
|
||||
help: Add the 'u' flag.
|
||||
note: The 'u' and 'v' flags enable Unicode-aware regular expression behavior and stricter pattern parsing.
|
||||
|
||||
⚠ eslint(require-unicode-regexp): Use the 'u' flag.
|
||||
╭─[require_unicode_regexp.tsx:1:1]
|
||||
1 │ new RegExp("foo", `g\uDC00`)
|
||||
· ────────────────────────────
|
||||
╰────
|
||||
help: Add the 'u' flag.
|
||||
note: The 'u' and 'v' flags enable Unicode-aware regular expression behavior and stricter pattern parsing.
|
||||
|
||||
⚠ eslint(require-unicode-regexp): Use the 'u' flag.
|
||||
╭─[require_unicode_regexp.tsx:1:1]
|
||||
1 │ /\a/
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ eslint(yoda): Require or disallow "Yoda" conditions
|
||||
╭─[yoda.tsx:1:5]
|
||||
1 │ if ("\uDBFF" <= x && x <= "\uD800") {}
|
||||
· ─────────────
|
||||
╰────
|
||||
help: Expected literal to be on the right side of <=.
|
||||
|
||||
⚠ eslint(yoda): Require or disallow "Yoda" conditions
|
||||
╭─[yoda.tsx:1:5]
|
||||
1 │ if (x <= 'foo' || 'bar' < x) {}
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ import(consistent-type-specifier-style): Prefer using inline type specifiers instead of a top-level type-only import.
|
||||
╭─[consistent_type_specifier_style.tsx:1:1]
|
||||
1 │ import type { Foo } from './\uD800.ts';
|
||||
· ───────────────────────────────────────
|
||||
╰────
|
||||
help: Replace top‐level import type with an inline type specifier.
|
||||
|
||||
⚠ import(consistent-type-specifier-style): Prefer using a top-level type-only import instead of inline type specifiers.
|
||||
╭─[consistent_type_specifier_style.tsx:1:10]
|
||||
1 │ import { type Foo, type Bar } from 'Foo'
|
||||
|
||||
@@ -2,6 +2,27 @@
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ import(first): Relative imports before absolute imports are prohibited
|
||||
╭─[index.ts:1:49]
|
||||
1 │ import { x } from './\uD800'; import { y } from 'bar'
|
||||
· ─────
|
||||
╰────
|
||||
help: Move absolute import above relative import
|
||||
|
||||
⚠ import(first): Relative imports before absolute imports are prohibited
|
||||
╭─[index.ts:1:50]
|
||||
1 │ import { x } from '../\uDC00'; import { y } from 'bar'
|
||||
· ─────
|
||||
╰────
|
||||
help: Move absolute import above relative import
|
||||
|
||||
⚠ import(first): Relative imports before absolute imports are prohibited
|
||||
╭─[index.ts:1:54]
|
||||
1 │ import { x } from '/\uD83D\uDE00'; import { y } from 'bar'
|
||||
· ─────
|
||||
╰────
|
||||
help: Move absolute import above relative import
|
||||
|
||||
⚠ import(first): Import statements must come first
|
||||
╭─[index.ts:3:15]
|
||||
2 │ export { x };
|
||||
|
||||
@@ -2,6 +2,27 @@
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ import(no-absolute-path): Do not import modules using an absolute path
|
||||
╭─[index.js:1:15]
|
||||
1 │ import _ from '/\uD800'
|
||||
· ─────────
|
||||
╰────
|
||||
help: Replace the absolute path with a relative path or a module alias.
|
||||
|
||||
⚠ import(no-absolute-path): Do not import modules using an absolute path
|
||||
╭─[index.js:1:17]
|
||||
1 │ var f = require('/\uDC00/x')
|
||||
· ───────────
|
||||
╰────
|
||||
help: Replace the absolute path with a relative path or a module alias.
|
||||
|
||||
⚠ import(no-absolute-path): Do not import modules using an absolute path
|
||||
╭─[index.js:1:10]
|
||||
1 │ require(['/\uD83D\uDE00'], function(){})
|
||||
· ───────────────
|
||||
╰────
|
||||
help: Replace the absolute path with a relative path or a module alias.
|
||||
|
||||
⚠ import(no-absolute-path): Do not import modules using an absolute path
|
||||
╭─[index.js:1:15]
|
||||
1 │ import _ from '/lodash'
|
||||
|
||||
@@ -2,6 +2,27 @@
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ import(no-nodejs-modules): Do not import Node.js builtin module `node:\ud800`
|
||||
╭─[no_nodejs_modules.tsx:1:1]
|
||||
1 │ import x from "node:\uD800"
|
||||
· ───────────────────────────
|
||||
╰────
|
||||
help: Use a browser-compatible alternative or add this module to the `allow` list if Node.js usage is intentional.
|
||||
|
||||
⚠ import(no-nodejs-modules): Do not import Node.js builtin module `node:\udc00`
|
||||
╭─[no_nodejs_modules.tsx:1:9]
|
||||
1 │ var x = require("node:\uDC00")
|
||||
· ──────────────────────
|
||||
╰────
|
||||
help: Use a browser-compatible alternative or add this module to the `allow` list if Node.js usage is intentional.
|
||||
|
||||
⚠ import(no-nodejs-modules): Do not import Node.js builtin module `node:😀`
|
||||
╭─[no_nodejs_modules.tsx:1:1]
|
||||
1 │ import x from "node:\uD83D\uDE00"
|
||||
· ─────────────────────────────────
|
||||
╰────
|
||||
help: Use a browser-compatible alternative or add this module to the `allow` list if Node.js usage is intentional.
|
||||
|
||||
⚠ import(no-nodejs-modules): Do not import Node.js builtin module `path`
|
||||
╭─[no_nodejs_modules.tsx:1:1]
|
||||
1 │ import path from "path"
|
||||
|
||||
@@ -2,6 +2,41 @@
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ import(no-relative-parent-imports): Relative imports from parent directories are not allowed
|
||||
╭─[index.js:1:17]
|
||||
1 │ import foo from "../\uD800"
|
||||
· ───────────
|
||||
╰────
|
||||
help: Move the file to the same directory, use dependency injection, or convert to a package.
|
||||
|
||||
⚠ import(no-relative-parent-imports): Relative imports from parent directories are not allowed
|
||||
╭─[index.js:1:17]
|
||||
1 │ import foo from "./../\uDC00.js"
|
||||
· ────────────────
|
||||
╰────
|
||||
help: Move the file to the same directory, use dependency injection, or convert to a package.
|
||||
|
||||
⚠ import(no-relative-parent-imports): Relative imports from parent directories are not allowed
|
||||
╭─[index.js:1:9]
|
||||
1 │ require("../\uD83D\uDE00")
|
||||
· ─────────────────
|
||||
╰────
|
||||
help: Move the file to the same directory, use dependency injection, or convert to a package.
|
||||
|
||||
⚠ import(no-relative-parent-imports): Relative imports from parent directories are not allowed
|
||||
╭─[index.js:1:8]
|
||||
1 │ import("../\uD800")
|
||||
· ───────────
|
||||
╰────
|
||||
help: Move the file to the same directory, use dependency injection, or convert to a package.
|
||||
|
||||
⚠ import(no-relative-parent-imports): Relative imports from parent directories are not allowed
|
||||
╭─[index.js:1:15]
|
||||
1 │ export * from "..\u002F\uDC00"
|
||||
· ────────────────
|
||||
╰────
|
||||
help: Move the file to the same directory, use dependency injection, or convert to a package.
|
||||
|
||||
⚠ import(no-relative-parent-imports): Relative imports from parent directories are not allowed
|
||||
╭─[index.js:1:17]
|
||||
1 │ import foo from "../plugin.js"
|
||||
|
||||
@@ -2,6 +2,27 @@
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ import(no-unassigned-import): Imported module should be assigned
|
||||
╭─[no-unassigned-import.js:1:1]
|
||||
1 │ import './\uD800.css'
|
||||
· ─────────────────────
|
||||
╰────
|
||||
help: Consider assigning the import to a variable or removing it if it's unused.
|
||||
|
||||
⚠ import(no-unassigned-import): Imported module should be assigned
|
||||
╭─[no-unassigned-import.js:1:1]
|
||||
1 │ import './\uD800.css'
|
||||
· ─────────────────────
|
||||
╰────
|
||||
help: Consider assigning the import to a variable or removing it if it's unused.
|
||||
|
||||
⚠ import(no-unassigned-import): A `require()` style import is forbidden.
|
||||
╭─[no-unassigned-import.js:1:1]
|
||||
1 │ require('./\uD800.css')
|
||||
· ───────────────────────
|
||||
╰────
|
||||
help: Consider assigning the import to a variable or removing it if it's unused.
|
||||
|
||||
⚠ import(no-unassigned-import): A `require()` style import is forbidden.
|
||||
╭─[no-unassigned-import.js:1:1]
|
||||
1 │ require('should')
|
||||
|
||||
@@ -2,6 +2,27 @@
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ jest(no-mocks-import): Mocks should not be manually imported from a `__mocks__` directory.
|
||||
╭─[no_mocks_import.tsx:1:9]
|
||||
1 │ require('./\uD83D\uDE00/__mocks__')
|
||||
· ──────────────────────────
|
||||
╰────
|
||||
help: Instead use `jest.mock` or `vi.mock` and import from the original module path.
|
||||
|
||||
⚠ jest(no-mocks-import): Mocks should not be manually imported from a `__mocks__` directory.
|
||||
╭─[no_mocks_import.tsx:1:9]
|
||||
1 │ require('./__mocks__/\uD800')
|
||||
· ────────────────────
|
||||
╰────
|
||||
help: Instead use `jest.mock` or `vi.mock` and import from the original module path.
|
||||
|
||||
⚠ jest(no-mocks-import): Mocks should not be manually imported from a `__mocks__` directory.
|
||||
╭─[no_mocks_import.tsx:1:9]
|
||||
1 │ require('\uDC00/__mocks__/x')
|
||||
· ────────────────────
|
||||
╰────
|
||||
help: Instead use `jest.mock` or `vi.mock` and import from the original module path.
|
||||
|
||||
⚠ jest(no-mocks-import): Mocks should not be manually imported from a `__mocks__` directory.
|
||||
╭─[no_mocks_import.tsx:1:9]
|
||||
1 │ require('./__mocks__')
|
||||
|
||||
@@ -2,6 +2,27 @@
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ jest(no-untyped-mock-factory): `jest.mock()` factories should not be used without an explicit type parameter.
|
||||
╭─[no_untyped_mock_factory.tsx:1:6]
|
||||
1 │ jest.mock('\uD800', () => ({}))
|
||||
· ────
|
||||
╰────
|
||||
help: Add a type parameter to the mock factory such as `typeof import("\ud800")`
|
||||
|
||||
⚠ jest(no-untyped-mock-factory): `jest.mock()` factories should not be used without an explicit type parameter.
|
||||
╭─[no_untyped_mock_factory.tsx:1:6]
|
||||
1 │ jest.doMock('a\uDC00b', () => ({}))
|
||||
· ──────
|
||||
╰────
|
||||
help: Add a type parameter to the mock factory such as `typeof import("a\udc00b")`
|
||||
|
||||
⚠ jest(no-untyped-mock-factory): `jest.mock()` factories should not be used without an explicit type parameter.
|
||||
╭─[no_untyped_mock_factory.tsx:1:6]
|
||||
1 │ jest.mock("it's", () => ({}))
|
||||
· ────
|
||||
╰────
|
||||
help: Add a type parameter to the mock factory such as `typeof import("it's")`
|
||||
|
||||
⚠ jest(no-untyped-mock-factory): `jest.mock()` factories should not be used without an explicit type parameter.
|
||||
╭─[no_untyped_mock_factory.tsx:2:22]
|
||||
1 │
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ jest(valid-title): test should match (?u)^\W
|
||||
╭─[valid_title.tsx:1:6]
|
||||
1 │ test('\uD800 foo', () => {});
|
||||
· ────────────
|
||||
╰────
|
||||
help: Make sure the title matches the `mustMatch` of your config file
|
||||
|
||||
⚠ jest(valid-title): Title must be a string
|
||||
╭─[valid_title.tsx:1:41]
|
||||
1 │ const String = { raw: () => 'foo' }; it(String.raw`foo`, () => {})
|
||||
|
||||
@@ -100,6 +100,20 @@ source: crates/oxc_linter/src/tester.rs
|
||||
╰────
|
||||
help: Use a `button` element instead of an `a` element.
|
||||
|
||||
⚠ jsx-a11y(anchor-is-valid): Use of incorrect `href` for the 'a' element.
|
||||
╭─[anchor_is_valid.tsx:1:2]
|
||||
1 │ <a href={"javascript:void(0)\uD800"} />
|
||||
· ─
|
||||
╰────
|
||||
help: Provide a correct `href` for the `a` element.
|
||||
|
||||
⚠ jsx-a11y(anchor-is-valid): Use of incorrect `href` for the 'a' element.
|
||||
╭─[anchor_is_valid.tsx:1:2]
|
||||
1 │ <a href={`\uD800javascript:void(0)`} />
|
||||
· ─
|
||||
╰────
|
||||
help: Provide a correct `href` for the `a` element.
|
||||
|
||||
⚠ jsx-a11y(anchor-is-valid): Missing `href` attribute for the `a` element.
|
||||
╭─[anchor_is_valid.tsx:1:2]
|
||||
1 │ <Link />
|
||||
|
||||
@@ -2,6 +2,30 @@
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ jsx-a11y(aria-proptypes): This is not a valid ARIA state and property value for 'aria-invalid'.
|
||||
╭─[aria_proptypes.tsx:1:6]
|
||||
1 │ <div aria-invalid={"\uD800"} />
|
||||
· ───────────────────────
|
||||
╰────
|
||||
help: The valid value for 'aria-invalid' is: one of the following tokens: grammar, false, spelling, true.
|
||||
You can find a list of valid ARIA state and property values at https://www.w3.org/TR/wai-aria/#x6-7-definitions-of-states-and-properties-all-aria-attributes
|
||||
|
||||
⚠ jsx-a11y(aria-proptypes): This is not a valid ARIA state and property value for 'aria-checked'.
|
||||
╭─[aria_proptypes.tsx:1:6]
|
||||
1 │ <div aria-checked={"true\uDC00"} />
|
||||
· ───────────────────────────
|
||||
╰────
|
||||
help: The valid value for 'aria-checked' is: 'true', 'false', or 'mixed'.
|
||||
You can find a list of valid ARIA state and property values at https://www.w3.org/TR/wai-aria/#x6-7-definitions-of-states-and-properties-all-aria-attributes
|
||||
|
||||
⚠ jsx-a11y(aria-proptypes): This is not a valid ARIA state and property value for 'aria-level'.
|
||||
╭─[aria_proptypes.tsx:1:6]
|
||||
1 │ <div aria-level={"\uD83D\uDE00"} />
|
||||
· ───────────────────────────
|
||||
╰────
|
||||
help: The valid value for 'aria-level' is: an integer value.
|
||||
You can find a list of valid ARIA state and property values at https://www.w3.org/TR/wai-aria/#x6-7-definitions-of-states-and-properties-all-aria-attributes
|
||||
|
||||
⚠ jsx-a11y(aria-proptypes): This is not a valid ARIA state and property value for 'aria-hidden'.
|
||||
╭─[aria_proptypes.tsx:1:6]
|
||||
1 │ <div aria-hidden="yes" />
|
||||
|
||||
@@ -9,6 +9,34 @@ source: crates/oxc_linter/src/tester.rs
|
||||
╰────
|
||||
help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don't need to use the words `image`, `photo`, or `picture` (or any specified custom words) in the `alt` prop.
|
||||
|
||||
⚠ jsx-a11y(img-redundant-alt): Redundant `alt` attribute.
|
||||
╭─[img_redundant_alt.tsx:1:6]
|
||||
1 │ <img alt={"\uD83D\uDE00 picture"} />;
|
||||
· ───
|
||||
╰────
|
||||
help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don't need to use the words `image`, `photo`, or `picture` (or any specified custom words) in the `alt` prop.
|
||||
|
||||
⚠ jsx-a11y(img-redundant-alt): Redundant `alt` attribute.
|
||||
╭─[img_redundant_alt.tsx:1:6]
|
||||
1 │ <img alt={"image \uD800"} />;
|
||||
· ───
|
||||
╰────
|
||||
help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don't need to use the words `image`, `photo`, or `picture` (or any specified custom words) in the `alt` prop.
|
||||
|
||||
⚠ jsx-a11y(img-redundant-alt): Redundant `alt` attribute.
|
||||
╭─[img_redundant_alt.tsx:1:6]
|
||||
1 │ <img alt={"\uDC00 Photo"} />;
|
||||
· ───
|
||||
╰────
|
||||
help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don't need to use the words `image`, `photo`, or `picture` (or any specified custom words) in the `alt` prop.
|
||||
|
||||
⚠ jsx-a11y(img-redundant-alt): Redundant `alt` attribute.
|
||||
╭─[img_redundant_alt.tsx:1:6]
|
||||
1 │ <img alt={"\uD800image"} />;
|
||||
· ───
|
||||
╰────
|
||||
help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don't need to use the words `image`, `photo`, or `picture` (or any specified custom words) in the `alt` prop.
|
||||
|
||||
⚠ jsx-a11y(img-redundant-alt): Redundant `alt` attribute.
|
||||
╭─[img_redundant_alt.tsx:1:6]
|
||||
1 │ <img alt='Picture of friend.' />;
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ node(no-mixed-requires): Do not mix core, module, file and computed requires.
|
||||
╭─[no_mixed_requires.tsx:1:1]
|
||||
1 │ var fs = require('fs'), a = require('\uD800')
|
||||
· ─────────────────────────────────────────────
|
||||
╰────
|
||||
|
||||
⚠ node(no-mixed-requires): Do not mix core, module, file and computed requires.
|
||||
╭─[no_mixed_requires.tsx:1:1]
|
||||
1 │ var a = require('./\uD83D\uDE00'), b = require('foo')
|
||||
· ─────────────────────────────────────────────────────
|
||||
╰────
|
||||
|
||||
⚠ node(no-mixed-requires): Do not mix 'require' and other declarations.
|
||||
╭─[no_mixed_requires.tsx:1:1]
|
||||
1 │ var fs = require('fs'), foo = 42
|
||||
|
||||
@@ -2,6 +2,20 @@
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ react(jsx-key): Duplicate key 'a\ud800' found in JSX elements
|
||||
╭─[jsx_key.tsx:1:45]
|
||||
1 │ const xs = [<span key={"a\uD800"} />, <span key={`a\uD800`} />];
|
||||
· ───────────────
|
||||
╰────
|
||||
help: Each child in a list should have a unique 'key' prop
|
||||
|
||||
⚠ react(jsx-key): Duplicate key 'foo' found in JSX elements
|
||||
╭─[jsx_key.tsx:1:41]
|
||||
1 │ const xs = [<span key={"foo"} />, <span key={`\u0066oo`} />];
|
||||
· ────────────────
|
||||
╰────
|
||||
help: Each child in a list should have a unique 'key' prop
|
||||
|
||||
⚠ react(jsx-key): Missing "key" prop for element in array.
|
||||
╭─[jsx_key.tsx:1:3]
|
||||
1 │ [<App />];
|
||||
@@ -157,6 +171,27 @@ source: crates/oxc_linter/src/tester.rs
|
||||
╰────
|
||||
help: Each child in a list should have a unique 'key' prop
|
||||
|
||||
⚠ react(jsx-key): Duplicate key '\ud800' found in JSX elements
|
||||
╭─[jsx_key.tsx:1:46]
|
||||
1 │ const spans = [<span key={"\uD800"}/>, <span key={"\uD800"}/>];
|
||||
· ──────────────
|
||||
╰────
|
||||
help: Each child in a list should have a unique 'key' prop
|
||||
|
||||
⚠ react(jsx-key): Duplicate key 'a\udc00' found in JSX elements
|
||||
╭─[jsx_key.tsx:1:47]
|
||||
1 │ const spans = [<span key={`a\uDC00`}/>, <span key={`a\uDC00`}/>];
|
||||
· ───────────────
|
||||
╰────
|
||||
help: Each child in a list should have a unique 'key' prop
|
||||
|
||||
⚠ react(jsx-key): Duplicate key '😀' found in JSX elements
|
||||
╭─[jsx_key.tsx:1:52]
|
||||
1 │ const div = <div><span key={"\uD83D\uDE00"}/><span key={"\uD83D\uDE00"}/></div>;
|
||||
· ────────────────────
|
||||
╰────
|
||||
help: Each child in a list should have a unique 'key' prop
|
||||
|
||||
⚠ react(jsx-key): Duplicate key 'notunique' found in JSX elements
|
||||
╭─[jsx_key.tsx:5:31]
|
||||
4 │ <span key="notunique"/>
|
||||
|
||||
@@ -9,6 +9,34 @@ source: crates/oxc_linter/src/tester.rs
|
||||
╰────
|
||||
help: add rel=`noreferrer` to the element
|
||||
|
||||
⚠ react(jsx-no-target-blank): Using target=`_blank` without rel=`noreferrer` (which implies rel=`noopener`) is a security risk in older browsers: see https://mathiasbynens.github.io/rel-noopener/#recommendations
|
||||
╭─[jsx_no_target_blank.tsx:1:11]
|
||||
1 │ <a target="_blank" href={"https://x/\uD800"}></a>
|
||||
· ────────
|
||||
╰────
|
||||
help: add rel=`noreferrer` to the element
|
||||
|
||||
⚠ react(jsx-no-target-blank): Using target=`_blank` without rel=`noreferrer` (which implies rel=`noopener`) is a security risk in older browsers: see https://mathiasbynens.github.io/rel-noopener/#recommendations
|
||||
╭─[jsx_no_target_blank.tsx:1:11]
|
||||
1 │ <a target="_blank" href="http://x.com" rel={"\uD800"}></a>
|
||||
· ────────
|
||||
╰────
|
||||
help: add rel=`noreferrer` to the element
|
||||
|
||||
⚠ react(jsx-no-target-blank): Using target=`_blank` without rel=`noreferrer` (which implies rel=`noopener`) is a security risk in older browsers: see https://mathiasbynens.github.io/rel-noopener/#recommendations
|
||||
╭─[jsx_no_target_blank.tsx:1:11]
|
||||
1 │ <a target="_blank" href={"//x/\uDC00"} rel={"\uDC00"}></a>
|
||||
· ────────
|
||||
╰────
|
||||
help: add rel=`noreferrer` to the element
|
||||
|
||||
⚠ react(jsx-no-target-blank): Using target=`_blank` without rel=`noreferrer` (which implies rel=`noopener`) is a security risk in older browsers: see https://mathiasbynens.github.io/rel-noopener/#recommendations
|
||||
╭─[jsx_no_target_blank.tsx:1:11]
|
||||
1 │ <a target="_blank" href={"https://x/\uD83D\uDE00"} rel={"noreferrer\uD800"}></a>
|
||||
· ────────
|
||||
╰────
|
||||
help: add rel=`noreferrer` to the element
|
||||
|
||||
⚠ react(jsx-no-target-blank): Using target=`_blank` without rel=`noreferrer` (which implies rel=`noopener`) is a security risk in older browsers: see https://mathiasbynens.github.io/rel-noopener/#recommendations
|
||||
╭─[jsx_no_target_blank.tsx:1:11]
|
||||
1 │ <a target="_blank" rel="" href="https://example.com/2"></a>
|
||||
|
||||
@@ -14,6 +14,24 @@ source: crates/oxc_linter/src/tester.rs
|
||||
· ──────────────────
|
||||
╰────
|
||||
|
||||
⚠ react(no-namespace): React component ns:\ud800 must not be in a namespace, as React does not support them.
|
||||
╭─[no_namespace.tsx:1:21]
|
||||
1 │ React.createElement("ns:\uD800")
|
||||
· ───────────
|
||||
╰────
|
||||
|
||||
⚠ react(no-namespace): React component \udc00:testcomponent must not be in a namespace, as React does not support them.
|
||||
╭─[no_namespace.tsx:1:21]
|
||||
1 │ React.createElement("\uDC00:testcomponent")
|
||||
· ──────────────────────
|
||||
╰────
|
||||
|
||||
⚠ react(no-namespace): React component 😀:\ud800 must not be in a namespace, as React does not support them.
|
||||
╭─[no_namespace.tsx:1:21]
|
||||
1 │ React.createElement("\uD83D\uDE00:\uD800")
|
||||
· ─────────────────────
|
||||
╰────
|
||||
|
||||
⚠ react(no-namespace): React component ns:testComponent must not be in a namespace, as React does not support them.
|
||||
╭─[no_namespace.tsx:1:2]
|
||||
1 │ <ns:testComponent />
|
||||
|
||||
@@ -2,6 +2,34 @@
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ typescript(no-require-imports): Expected "import" statement instead of "require" call
|
||||
╭─[no_require_imports.ts:1:13]
|
||||
1 │ const pkg = require('a\uD800b.json');
|
||||
· ────────────────────────
|
||||
╰────
|
||||
help: Do not use CommonJS `require` calls
|
||||
|
||||
⚠ typescript(no-require-imports): Expected "import" statement instead of "require" call
|
||||
╭─[no_require_imports.ts:1:1]
|
||||
1 │ import pkg = require('a\uD800b.json');
|
||||
· ──────────────────────────────────────
|
||||
╰────
|
||||
help: Do not use CommonJS `require` calls
|
||||
|
||||
⚠ typescript(no-require-imports): Expected "import" statement instead of "require" call
|
||||
╭─[no_require_imports.ts:1:13]
|
||||
1 │ const pkg = require(`a\uD800b.json`);
|
||||
· ────────────────────────
|
||||
╰────
|
||||
help: Do not use CommonJS `require` calls
|
||||
|
||||
⚠ typescript(no-require-imports): Expected "import" statement instead of "require" call
|
||||
╭─[no_require_imports.ts:1:13]
|
||||
1 │ const pkg = require(`./${name}.json`);
|
||||
· ─────────────────────────
|
||||
╰────
|
||||
help: Do not use CommonJS `require` calls
|
||||
|
||||
⚠ typescript(no-require-imports): Expected "import" statement instead of "require" call
|
||||
╭─[no_require_imports.ts:1:11]
|
||||
1 │ var lib = require('lib');
|
||||
|
||||
@@ -344,3 +344,31 @@ source: crates/oxc_linter/src/tester.rs
|
||||
· ───
|
||||
╰────
|
||||
help: The `console.log()` method and similar methods join the parameters with a space so adding a leading/trailing space to a parameter, results in two spaces being added.
|
||||
|
||||
⚠ unicorn(no-console-spaces): Do not use trailing spaces with `console.log` parameters
|
||||
╭─[no_console_spaces.tsx:1:14]
|
||||
1 │ console.log("\uD800 ", "def");
|
||||
· ───────
|
||||
╰────
|
||||
help: The `console.log()` method and similar methods join the parameters with a space so adding a leading/trailing space to a parameter, results in two spaces being added.
|
||||
|
||||
⚠ unicorn(no-console-spaces): Do not use leading spaces with `console.log` parameters
|
||||
╭─[no_console_spaces.tsx:1:21]
|
||||
1 │ console.log("abc", " \uDC00");
|
||||
· ───────
|
||||
╰────
|
||||
help: The `console.log()` method and similar methods join the parameters with a space so adding a leading/trailing space to a parameter, results in two spaces being added.
|
||||
|
||||
⚠ unicorn(no-console-spaces): Do not use leading spaces with `console.log` parameters
|
||||
╭─[no_console_spaces.tsx:1:19]
|
||||
1 │ console.log("_", " \uD83D\uDE00 ", "_");
|
||||
· ──────────────
|
||||
╰────
|
||||
help: The `console.log()` method and similar methods join the parameters with a space so adding a leading/trailing space to a parameter, results in two spaces being added.
|
||||
|
||||
⚠ unicorn(no-console-spaces): Do not use trailing spaces with `console.log` parameters
|
||||
╭─[no_console_spaces.tsx:1:19]
|
||||
1 │ console.log("_", " \uD83D\uDE00 ", "_");
|
||||
· ──────────────
|
||||
╰────
|
||||
help: The `console.log()` method and similar methods join the parameters with a space so adding a leading/trailing space to a parameter, results in two spaces being added.
|
||||
@@ -485,3 +485,9 @@ source: crates/oxc_linter/src/tester.rs
|
||||
· ────────────
|
||||
╰────
|
||||
help: Replace `(await promise).getAttribute("data-foo")` with `(await promise).dataset.foo`.
|
||||
|
||||
⚠ unicorn(prefer-dom-node-dataset): Prefer using `dataset` over `setAttribute`.
|
||||
╭─[prefer_dom_node_dataset.mjs:1:4]
|
||||
1 │ el.setAttribute("data-\uD800", "x");
|
||||
· ────────────
|
||||
╰────
|
||||
@@ -1180,3 +1180,26 @@ source: crates/oxc_linter/src/tester.rs
|
||||
3 │ export {bar} from './foo.json' with { type: 'unknown' };
|
||||
╰────
|
||||
help: use `export ... from ...;`
|
||||
|
||||
⚠ unicorn(prefer-export-from): Prefer re-exporting directly from the source module.
|
||||
╭─[prefer_export_from.tsx:1:1]
|
||||
1 │ import {a} from "\uD800";
|
||||
· ────────────┬────────────
|
||||
· ╰── Imported here.
|
||||
2 │ export {a};
|
||||
· ┬
|
||||
· ╰── Re-exported here.
|
||||
╰────
|
||||
help: use `export ... from ...;`
|
||||
|
||||
⚠ unicorn(prefer-export-from): Prefer re-exporting directly from the source module.
|
||||
╭─[prefer_export_from.tsx:1:1]
|
||||
1 │ import {a} from "\uD800";
|
||||
· ────────────┬────────────
|
||||
· ╰── Imported here.
|
||||
2 │ export {a};
|
||||
· ┬
|
||||
· ╰── Re-exported here.
|
||||
3 │ export {b} from "\uD800";
|
||||
╰────
|
||||
help: use `export ... from ...;`
|
||||
@@ -14,6 +14,27 @@ source: crates/oxc_linter/src/tester.rs
|
||||
╰────
|
||||
help: Rewrite this `if`/`else` as a ternary expression.
|
||||
|
||||
⚠ unicorn(prefer-ternary): Prefer ternary expressions over simple `if-else` statements.
|
||||
╭─[prefer_ternary.tsx:1:1]
|
||||
1 │ if (test) { obj["\uD800"] = a; } else { obj["\uD800"] = b; }
|
||||
· ────────────────────────────────────────────────────────────
|
||||
╰────
|
||||
help: Rewrite this `if`/`else` as a ternary expression.
|
||||
|
||||
⚠ unicorn(prefer-ternary): Prefer ternary expressions over simple `if-else` statements.
|
||||
╭─[prefer_ternary.tsx:1:1]
|
||||
1 │ if (test) { obj["\uD800" + "x"] = a; } else { obj[`\uD800x`] = b; }
|
||||
· ───────────────────────────────────────────────────────────────────
|
||||
╰────
|
||||
help: Rewrite this `if`/`else` as a ternary expression.
|
||||
|
||||
⚠ unicorn(prefer-ternary): Prefer ternary expressions over simple `if-else` statements.
|
||||
╭─[prefer_ternary.tsx:1:1]
|
||||
1 │ if (test) { obj["\uD83D" + "\uDE00"] = a; } else { obj["\uD83D\uDE00"] = b; }
|
||||
· ─────────────────────────────────────────────────────────────────────────────
|
||||
╰────
|
||||
help: Rewrite this `if`/`else` as a ternary expression.
|
||||
|
||||
⚠ unicorn(prefer-ternary): Prefer ternary expressions over simple `if-else` statements.
|
||||
╭─[prefer_ternary.tsx:2:17]
|
||||
1 │ async function unicorn() {
|
||||
|
||||
@@ -2,6 +2,27 @@
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ vitest(no-mocks-import): Mocks should not be manually imported from a `__mocks__` directory.
|
||||
╭─[no_mocks_import.tsx:1:9]
|
||||
1 │ require('./\uD83D\uDE00/__mocks__')
|
||||
· ──────────────────────────
|
||||
╰────
|
||||
help: Instead use `jest.mock` or `vi.mock` and import from the original module path.
|
||||
|
||||
⚠ vitest(no-mocks-import): Mocks should not be manually imported from a `__mocks__` directory.
|
||||
╭─[no_mocks_import.tsx:1:9]
|
||||
1 │ require('./__mocks__/\uD800')
|
||||
· ────────────────────
|
||||
╰────
|
||||
help: Instead use `jest.mock` or `vi.mock` and import from the original module path.
|
||||
|
||||
⚠ vitest(no-mocks-import): Mocks should not be manually imported from a `__mocks__` directory.
|
||||
╭─[no_mocks_import.tsx:1:9]
|
||||
1 │ require('\uDC00/__mocks__/x')
|
||||
· ────────────────────
|
||||
╰────
|
||||
help: Instead use `jest.mock` or `vi.mock` and import from the original module path.
|
||||
|
||||
⚠ vitest(no-mocks-import): Mocks should not be manually imported from a `__mocks__` directory.
|
||||
╭─[no_mocks_import.tsx:1:9]
|
||||
1 │ require('./__mocks__')
|
||||
|
||||
@@ -2,6 +2,20 @@
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ vitest(prefer-import-in-mock): Mocked modules must be dynamic imported.
|
||||
╭─[prefer_import_in_mock.tsx:1:9]
|
||||
1 │ vi.mock("\uD800")
|
||||
· ────────
|
||||
╰────
|
||||
help: Dynamic import improves the type information and IntelliSense. Substitute `"\ud800"` with `import("\ud800")`
|
||||
|
||||
⚠ vitest(prefer-import-in-mock): Mocked modules must be dynamic imported.
|
||||
╭─[prefer_import_in_mock.tsx:1:11]
|
||||
1 │ vi.doMock("a\uDC00b", () => ({}))
|
||||
· ──────────────────────
|
||||
╰────
|
||||
help: Dynamic import improves the type information and IntelliSense. Substitute `"a\udc00b"` with `import("a\udc00b")`
|
||||
|
||||
⚠ vitest(prefer-import-in-mock): Mocked modules must be dynamic imported.
|
||||
╭─[prefer_import_in_mock.tsx:1:9]
|
||||
1 │ vi.mock('foo', () => {})
|
||||
|
||||
@@ -2,6 +2,31 @@
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ vue(component-definition-name-casing): Property name "foo-\ud800" is not PascalCase.
|
||||
╭─[component_definition_name_casing.tsx:3:29]
|
||||
2 │ export default {
|
||||
3 │ name: "foo-\uD800"
|
||||
· ────────────
|
||||
4 │ }
|
||||
╰────
|
||||
|
||||
⚠ vue(component-definition-name-casing): Property name "\udc00_bar" is not PascalCase.
|
||||
╭─[component_definition_name_casing.tsx:3:29]
|
||||
2 │ export default {
|
||||
3 │ name: `\uDC00_bar`
|
||||
· ────────────
|
||||
4 │ }
|
||||
╰────
|
||||
|
||||
⚠ vue(component-definition-name-casing): Property name "Foo😀" is not kebab-case.
|
||||
╭─[component_definition_name_casing.tsx:3:29]
|
||||
2 │ export default {
|
||||
3 │ name: "Foo\uD83D\uDE00"
|
||||
· ─────────────────
|
||||
4 │ }
|
||||
╰────
|
||||
help: Replace `Foo\uD83D\uDE00` with `foo😀`.
|
||||
|
||||
⚠ vue(component-definition-name-casing): Property name "foo-bar" is not PascalCase.
|
||||
╭─[component_definition_name_casing.tsx:3:29]
|
||||
2 │ export default {
|
||||
|
||||
@@ -114,6 +114,14 @@ source: crates/oxc_linter/src/tester.rs
|
||||
5 │ })
|
||||
╰────
|
||||
|
||||
⚠ vue(no-reserved-keys): Key `_\ud800` is reserved in `data` group.
|
||||
╭─[no_reserved_keys.tsx:4:34]
|
||||
3 │ new Vue({
|
||||
4 │ data: () => ({ "_\uD800": 1 })
|
||||
· ─────────
|
||||
5 │ })
|
||||
╰────
|
||||
|
||||
⚠ vue(no-reserved-keys): Key `bar` is reserved.
|
||||
╭─[no_reserved_keys.tsx:4:26]
|
||||
3 │ new Vue({
|
||||
|
||||
@@ -2,6 +2,38 @@
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ vue(prop-name-casing): Prop '\udc00_text' is not in camelCase.
|
||||
╭─[prop_name_casing.tsx:5:23]
|
||||
4 │ props: {
|
||||
5 │ "\uDC00_text": String,
|
||||
· ─────────────
|
||||
6 │ }
|
||||
╰────
|
||||
|
||||
⚠ vue(prop-name-casing): Prop 'greeting-\ud800' is not in camelCase.
|
||||
╭─[prop_name_casing.tsx:4:31]
|
||||
3 │ export default {
|
||||
4 │ props: ["greeting-\uD800"]
|
||||
· ─────────────────
|
||||
5 │ }
|
||||
╰────
|
||||
|
||||
⚠ vue(prop-name-casing): Prop '\udc00_text' is not in camelCase.
|
||||
╭─[prop_name_casing.tsx:4:32]
|
||||
3 │ export default {
|
||||
4 │ props: { "\uDC00_text": String }
|
||||
· ─────────────
|
||||
5 │ }
|
||||
╰────
|
||||
|
||||
⚠ vue(prop-name-casing): Prop 'Greeting😀' is not in camelCase.
|
||||
╭─[prop_name_casing.tsx:4:33]
|
||||
3 │ export default {
|
||||
4 │ props: { [`Greeting\uD83D\uDE00`]: String }
|
||||
· ──────────────────────
|
||||
5 │ }
|
||||
╰────
|
||||
|
||||
⚠ vue(prop-name-casing): Prop 'greeting_text' is not in camelCase.
|
||||
╭─[prop_name_casing.tsx:5:25]
|
||||
4 │ props: {
|
||||
|
||||
@@ -1,25 +1,44 @@
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_ast::ast::{BinaryOperator, Expression};
|
||||
use oxc_str::{JSStr, JSStrBuilder};
|
||||
|
||||
/// Resolve a side-effect-free string expression made from string literals, template literals,
|
||||
/// and `+` concatenation. Returns `None` when any part cannot be determined statically.
|
||||
pub fn static_string_value(expression: &Expression<'_>) -> Option<String> {
|
||||
///
|
||||
/// A plain string literal is borrowed.
|
||||
/// A concatenation is built in `allocator`.
|
||||
/// Lone surrogates are preserved, and a pair split across parts is joined.
|
||||
pub fn static_string_value<'a>(
|
||||
expression: &Expression<'a>,
|
||||
allocator: &'a Allocator,
|
||||
) -> Option<JSStr<'a>> {
|
||||
if let Expression::StringLiteral(literal) = expression.get_inner_expression() {
|
||||
return Some(literal.value);
|
||||
}
|
||||
let mut builder = JSStrBuilder::new_in(&allocator);
|
||||
push_static_string_value(expression, &mut builder)?;
|
||||
Some(builder.into_js_str())
|
||||
}
|
||||
|
||||
fn push_static_string_value(
|
||||
expression: &Expression<'_>,
|
||||
builder: &mut JSStrBuilder<'_>,
|
||||
) -> Option<()> {
|
||||
match expression.get_inner_expression() {
|
||||
Expression::StringLiteral(literal) => literal.value.as_str().map(str::to_owned),
|
||||
Expression::StringLiteral(literal) => builder.push_js_str(literal.value),
|
||||
Expression::TemplateLiteral(template) => {
|
||||
let mut value = String::new();
|
||||
for (index, quasi) in template.quasis.iter().enumerate() {
|
||||
value.push_str(quasi.value.cooked?.as_str()?);
|
||||
builder.push_js_str(quasi.value.cooked?);
|
||||
if let Some(expr) = template.expressions.get(index) {
|
||||
value.push_str(&static_string_value(expr)?);
|
||||
push_static_string_value(expr, builder)?;
|
||||
}
|
||||
}
|
||||
Some(value)
|
||||
}
|
||||
Expression::BinaryExpression(binary) if binary.operator == BinaryOperator::Addition => {
|
||||
let mut value = static_string_value(&binary.left)?;
|
||||
value.push_str(&static_string_value(&binary.right)?);
|
||||
Some(value)
|
||||
push_static_string_value(&binary.left, builder)?;
|
||||
push_static_string_value(&binary.right, builder)?;
|
||||
}
|
||||
_ => None,
|
||||
_ => return None,
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
//! prop name, etc.) is in a particular casing.
|
||||
|
||||
use convert_case::{Boundary, Case, Converter};
|
||||
use oxc_str::JSStr;
|
||||
|
||||
/// Returns true if `s` contains any character that is not allowed in any of
|
||||
/// the casings recognised by eslint-plugin-vue.
|
||||
@@ -10,73 +11,77 @@ use convert_case::{Boundary, Case, Converter};
|
||||
/// Mirrors upstream `hasSymbols`. The character class excludes ` `, `$`,
|
||||
/// `-`, `_` deliberately — `-` and `_` are case-specific word separators
|
||||
/// and `$` is allowed in JavaScript identifiers (e.g. `$actionEl`).
|
||||
pub fn has_symbols(s: &str) -> bool {
|
||||
s.chars().any(|c| {
|
||||
matches!(
|
||||
c,
|
||||
'!' | '"'
|
||||
| '#'
|
||||
| '%'
|
||||
| '&'
|
||||
| '\''
|
||||
| '('
|
||||
| ')'
|
||||
| '*'
|
||||
| '+'
|
||||
| ','
|
||||
| '.'
|
||||
| '/'
|
||||
| ':'
|
||||
| ';'
|
||||
| '<'
|
||||
| '='
|
||||
| '>'
|
||||
| '?'
|
||||
| '@'
|
||||
| '['
|
||||
| '\\'
|
||||
| ']'
|
||||
| '^'
|
||||
| '`'
|
||||
| '{'
|
||||
| '|'
|
||||
| '}'
|
||||
)
|
||||
})
|
||||
///
|
||||
/// A lone surrogate is not a symbol, so it is treated like any other non-ASCII code point.
|
||||
pub fn has_symbols(s: JSStr<'_>) -> bool {
|
||||
s.contains(is_symbol)
|
||||
}
|
||||
|
||||
fn is_symbol(c: char) -> bool {
|
||||
matches!(
|
||||
c,
|
||||
'!' | '"'
|
||||
| '#'
|
||||
| '%'
|
||||
| '&'
|
||||
| '\''
|
||||
| '('
|
||||
| ')'
|
||||
| '*'
|
||||
| '+'
|
||||
| ','
|
||||
| '.'
|
||||
| '/'
|
||||
| ':'
|
||||
| ';'
|
||||
| '<'
|
||||
| '='
|
||||
| '>'
|
||||
| '?'
|
||||
| '@'
|
||||
| '['
|
||||
| '\\'
|
||||
| ']'
|
||||
| '^'
|
||||
| '`'
|
||||
| '{'
|
||||
| '|'
|
||||
| '}'
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns true if `s` contains any ASCII uppercase letter.
|
||||
pub fn has_upper(s: &str) -> bool {
|
||||
s.chars().any(|c| c.is_ascii_uppercase())
|
||||
pub fn has_upper(s: JSStr<'_>) -> bool {
|
||||
s.as_bytes().iter().any(u8::is_ascii_uppercase)
|
||||
}
|
||||
|
||||
pub fn is_pascal_case(s: &str) -> bool {
|
||||
pub fn is_pascal_case(s: JSStr<'_>) -> bool {
|
||||
!has_symbols(s)
|
||||
&& !s.chars().next().is_some_and(|c| c.is_ascii_lowercase())
|
||||
&& !s.chars().any(|c| matches!(c, '-' | '_') || c.is_whitespace())
|
||||
&& !s.starts_with(|c: char| c.is_ascii_lowercase())
|
||||
&& !s.contains(|c: char| matches!(c, '-' | '_') || c.is_whitespace())
|
||||
}
|
||||
|
||||
pub fn is_kebab_case(s: &str) -> bool {
|
||||
pub fn is_kebab_case(s: JSStr<'_>) -> bool {
|
||||
if has_upper(s) || has_symbols(s) || s.starts_with('-') {
|
||||
return false;
|
||||
}
|
||||
if s.contains('_') || s.contains("--") || s.chars().any(char::is_whitespace) {
|
||||
if s.contains('_') || s.contains("--") || s.contains(char::is_whitespace) {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn is_camel_case(s: &str) -> bool {
|
||||
pub fn is_camel_case(s: JSStr<'_>) -> bool {
|
||||
!has_symbols(s)
|
||||
&& !s.chars().next().is_some_and(|c| c.is_ascii_uppercase())
|
||||
&& !s.chars().any(|c| matches!(c, '-' | '_') || c.is_whitespace())
|
||||
&& !s.starts_with(|c: char| c.is_ascii_uppercase())
|
||||
&& !s.contains(|c: char| matches!(c, '-' | '_') || c.is_whitespace())
|
||||
}
|
||||
|
||||
pub fn is_snake_case(s: &str) -> bool {
|
||||
pub fn is_snake_case(s: JSStr<'_>) -> bool {
|
||||
if has_upper(s) || has_symbols(s) {
|
||||
return false;
|
||||
}
|
||||
if s.contains('-') || s.contains("__") || s.chars().any(char::is_whitespace) {
|
||||
if s.contains('-') || s.contains("__") || s.contains(char::is_whitespace) {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
@@ -108,7 +113,7 @@ fn regex_word_before_upper(graphemes: &[&str]) -> bool {
|
||||
/// - if input is already PascalCase: lowercase the first char
|
||||
/// - else: replace `[-_](\w)` with `\w` uppercased
|
||||
pub fn camel_case(s: &str) -> String {
|
||||
if is_pascal_case(s) {
|
||||
if is_pascal_case(JSStr::from(s)) {
|
||||
let mut chars = s.chars();
|
||||
return match chars.next() {
|
||||
Some(c) => c.to_lowercase().collect::<String>() + chars.as_str(),
|
||||
|
||||
+18
-13
@@ -12,20 +12,25 @@
|
||||
//!
|
||||
//! # Consuming `JSStr`
|
||||
//!
|
||||
//! A consumer that needs `str` decides what a lone surrogate means for its
|
||||
//! check, and two policies cover the cases in practice. Checks over names,
|
||||
//! paths, and specifiers decline when [`JSStr::as_str`] returns `None`: a
|
||||
//! fixed name set or a well-known path never contains a lone surrogate, so
|
||||
//! declining changes nothing. Comparisons with fixed names use `JSStr`'s
|
||||
//! `PartialEq<&str>` directly and need no conversion. A value with a lone
|
||||
//! surrogate is not matched against user-configured patterns; rules skip
|
||||
//! that value. A value shown to a person uses the `Debug` form, which
|
||||
//! escapes the surrogate instead of replacing it.
|
||||
//! A consumer that would otherwise need `str` decides what a lone surrogate
|
||||
//! means for its check:
|
||||
//!
|
||||
//! Declining is only safe where skipping the work is conservative, as for an
|
||||
//! optional optimization or a lookup that cannot match. Code that decides
|
||||
//! whether a program is valid, whether a rewrite is safe, or what code to emit
|
||||
//! must handle the value instead of skipping it.
|
||||
//! - Comparisons with fixed names use `JSStr`'s `PartialEq<&str>` directly and
|
||||
//! need no conversion. A value with a lone surrogate never equals one.
|
||||
//! - Searches for fixed text, such as a prefix or a substring, use the
|
||||
//! [`JSStrPattern`] methods ([`JSStr::contains`], [`JSStr::starts_with`],
|
||||
//! [`JSStr::find`], and others), which search the value without converting it.
|
||||
//! - User-configured patterns, such as regexes and globs, take UTF-8 text, so
|
||||
//! they match only [`JSStr::as_str`]. A value with a lone surrogate matches no
|
||||
//! pattern.
|
||||
//! - A value shown to a person uses [`JSStr::display`] or the `Debug` form,
|
||||
//! which escape the surrogate instead of replacing it.
|
||||
//!
|
||||
//! Declining when [`JSStr::as_str`] returns `None` is only safe where skipping
|
||||
//! the work is conservative, as for an optional optimization or a lookup that
|
||||
//! cannot match. Code that decides whether a program is valid, whether a
|
||||
//! rewrite is safe, or what code to emit must handle the value instead of
|
||||
//! skipping it.
|
||||
|
||||
mod compact_str;
|
||||
mod ident;
|
||||
|
||||
@@ -9,7 +9,7 @@ use oxc_ast::ast::{
|
||||
ExportAllDeclaration, ExportFromDeclaration, Expression, ImportDeclaration, ImportExpression,
|
||||
StringLiteral, TemplateLiteral,
|
||||
};
|
||||
use oxc_str::Str;
|
||||
use oxc_str::{JSStr, JSStrBuilder, Str};
|
||||
use oxc_traverse::Traverse;
|
||||
|
||||
use crate::{TypeScriptOptions, context::TraverseCtx, state::TransformState};
|
||||
@@ -32,13 +32,7 @@ fn rewritten_specifier<'a>(
|
||||
}
|
||||
|
||||
let (without_extension, extension) = value.rsplit_once('.')?;
|
||||
|
||||
let replace = match extension {
|
||||
"mts" => ".mjs",
|
||||
"cts" => ".cjs",
|
||||
"ts" | "tsx" => ".js",
|
||||
_ => return None,
|
||||
};
|
||||
let replace = replacement_extension(extension)?;
|
||||
|
||||
Some(if mode.is_remove() {
|
||||
Str::from(without_extension)
|
||||
@@ -47,16 +41,58 @@ fn rewritten_specifier<'a>(
|
||||
})
|
||||
}
|
||||
|
||||
fn replacement_extension(extension: &str) -> Option<&'static str> {
|
||||
match extension {
|
||||
"mts" => Some(".mjs"),
|
||||
"cts" => Some(".cjs"),
|
||||
"ts" | "tsx" => Some(".js"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [`rewritten_specifier`], for a value that may contain lone surrogates.
|
||||
/// The part before the extension is copied unchanged.
|
||||
fn rewritten_js_specifier<'a>(
|
||||
value: JSStr<'a>,
|
||||
mode: RewriteExtensionsMode,
|
||||
ctx: &TraverseCtx<'a>,
|
||||
) -> Option<JSStr<'a>> {
|
||||
if let Some(value) = value.as_str() {
|
||||
return rewritten_specifier(value, mode, ctx).map(JSStr::from);
|
||||
}
|
||||
|
||||
if !value.contains(|c| c == '/' || c == '\\') {
|
||||
return None;
|
||||
}
|
||||
|
||||
let dot = value.rfind('.')?;
|
||||
// An extension with a lone surrogate is not valid UTF-8 and matches no rewrite.
|
||||
let extension = str::from_utf8(&value.as_bytes()[dot + 1..]).ok()?;
|
||||
let replace = replacement_extension(extension)?;
|
||||
|
||||
let mut builder = JSStrBuilder::with_capacity_in(dot + replace.len(), ctx);
|
||||
let mut len = 0;
|
||||
for ch in value.chars() {
|
||||
if len == dot {
|
||||
break;
|
||||
}
|
||||
len += ch.len_bytes();
|
||||
builder.push_js_char(ch);
|
||||
}
|
||||
if !mode.is_remove() {
|
||||
builder.push_str(replace);
|
||||
}
|
||||
Some(builder.into_js_str())
|
||||
}
|
||||
|
||||
impl TypeScriptRewriteExtensions {
|
||||
pub fn new(options: &TypeScriptOptions) -> Option<Self> {
|
||||
options.rewrite_import_extensions.map(|mode| Self { mode })
|
||||
}
|
||||
|
||||
pub fn rewrite_extensions<'a>(&self, source: &mut StringLiteral<'a>, ctx: &TraverseCtx<'a>) {
|
||||
if let Some(value) = source.value.as_str()
|
||||
&& let Some(rewritten) = rewritten_specifier(value, self.mode, ctx)
|
||||
{
|
||||
source.value = rewritten.into();
|
||||
if let Some(rewritten) = rewritten_js_specifier(source.value, self.mode, ctx) {
|
||||
source.value = rewritten;
|
||||
source.raw = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
commit: 4f4ef5d4
|
||||
|
||||
Passed: 304/433
|
||||
Passed: 306/435
|
||||
|
||||
# All Passed:
|
||||
* babel-plugin-transform-class-static-block
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import a from "./x\uD800.ts";
|
||||
import b from "./y.ts";
|
||||
import "./x\uD800.mts";
|
||||
import "\uD800/a.tsx";
|
||||
// An extension with a lone surrogate should not be transformed.
|
||||
import "./a.ts\uD800";
|
||||
// Bare import should not be rewritten.
|
||||
import "\uD800.ts";
|
||||
console.log(a, b);
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sourceType": "module",
|
||||
"presets": [["typescript", { "rewriteImportExtensions": "remove" }]]
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import a from "./x\uD800";
|
||||
import b from "./y";
|
||||
import "./x\uD800";
|
||||
import "\uD800/a";
|
||||
// An extension with a lone surrogate should not be transformed.
|
||||
import "./a.ts\uD800";
|
||||
// Bare import should not be rewritten.
|
||||
import "\uD800.ts";
|
||||
console.log(a, b);
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import a from "./x\uD800.ts";
|
||||
import b from "./y.ts";
|
||||
import "./x\uD800.mts";
|
||||
import "\uD800/a.tsx";
|
||||
// An extension with a lone surrogate should not be transformed.
|
||||
import "./a.ts\uD800";
|
||||
// Bare import should not be rewritten.
|
||||
import "\uD800.ts";
|
||||
console.log(a, b);
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sourceType": "module",
|
||||
"presets": [["typescript", { "rewriteImportExtensions": "rewrite" }]]
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import a from "./x\uD800.js";
|
||||
import b from "./y.js";
|
||||
import "./x\uD800.mjs";
|
||||
import "\uD800/a.js";
|
||||
// An extension with a lone surrogate should not be transformed.
|
||||
import "./a.ts\uD800";
|
||||
// Bare import should not be rewritten.
|
||||
import "\uD800.ts";
|
||||
console.log(a, b);
|
||||
Reference in new issue
Block a user