kaykdm 2024-01-11 12:12:46 +09:00 committed by GitHub
parent d4acd140ad
commit c5887bcb2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 373 additions and 0 deletions

View file

@ -111,6 +111,7 @@ mod typescript {
pub mod no_unsafe_declaration_merging;
pub mod no_var_requires;
pub mod prefer_as_const;
pub mod triple_slash_reference;
}
mod jest {
@ -378,6 +379,7 @@ oxc_macros::declare_all_lint_rules! {
typescript::no_unsafe_declaration_merging,
typescript::no_var_requires,
typescript::prefer_as_const,
typescript::triple_slash_reference,
jest::expect_expect,
jest::max_expects,
jest::no_alias_methods,

View file

@ -0,0 +1,327 @@
use std::collections::HashMap;
use oxc_ast::{
ast::{Declaration, ModuleDeclaration, Statement, TSModuleReference},
AstKind,
};
use oxc_diagnostics::{
miette::{self, Diagnostic},
thiserror::{self, Error},
};
use oxc_macros::declare_oxc_lint;
use oxc_span::{GetSpan, Span};
use crate::{context::LintContext, rule::Rule};
#[derive(Debug, Error, Diagnostic)]
#[error("typescript-eslint(triple-slash-reference): Do not use a triple slash reference for {0}, use `import` style instead.")]
#[diagnostic(severity(warning), help("Use of triple-slash reference type directives is generally discouraged in favor of ECMAScript Module imports."))]
struct TripleSlashReferenceDiagnostic(String, #[label] pub Span);
#[derive(Debug, Default, Clone)]
pub struct TripleSlashReference(Box<TripleSlashReferenceConfig>);
#[derive(Debug, Clone, Default)]
pub struct TripleSlashReferenceConfig {
lib: LibOption,
path: PathOption,
types: TypesOption,
}
#[derive(Debug, Default, Clone, PartialEq)]
enum LibOption {
#[default]
Always,
Never,
}
#[derive(Debug, Default, Clone, PartialEq)]
enum PathOption {
Always,
#[default]
Never,
}
#[derive(Debug, Default, Clone, PartialEq)]
enum TypesOption {
Always,
Never,
#[default]
PreferImport,
}
impl std::ops::Deref for TripleSlashReference {
type Target = TripleSlashReferenceConfig;
fn deref(&self) -> &Self::Target {
&self.0
}
}
declare_oxc_lint!(
/// ### What it does
/// Disallow certain triple slash directives in favor of ES6-style import declarations.
///
/// ### Why is this bad?
/// Use of triple-slash reference type directives is generally discouraged in favor of ECMAScript Module imports.
///
/// ### Example
/// ```javascript
/// /// <reference lib="code" />
/// globalThis.value;
/// ```
TripleSlashReference,
correctness
);
impl Rule for TripleSlashReference {
fn from_configuration(value: serde_json::Value) -> Self {
let options: Option<&serde_json::Value> = value.get(0);
Self(Box::new(TripleSlashReferenceConfig {
lib: options
.and_then(|x| x.get("lib"))
.and_then(serde_json::Value::as_str)
.map_or_else(LibOption::default, |value| match value {
"always" => LibOption::Always,
"never" => LibOption::Never,
_ => LibOption::default(),
}),
path: options
.and_then(|x| x.get("path"))
.and_then(serde_json::Value::as_str)
.map_or_else(PathOption::default, |value| match value {
"always" => PathOption::Always,
"never" => PathOption::Never,
_ => PathOption::default(),
}),
types: options
.and_then(|x| x.get("types"))
.and_then(serde_json::Value::as_str)
.map_or_else(TypesOption::default, |value| match value {
"always" => TypesOption::Always,
"never" => TypesOption::Never,
"prefer-import" => TypesOption::PreferImport,
_ => TypesOption::default(),
}),
}))
}
fn run_once(&self, ctx: &LintContext) {
let Some(root) = ctx.nodes().iter().next() else { return };
let AstKind::Program(program) = root.kind() else { return };
// We don't need to iterate over all comments since Triple-slash directives are only valid at the top of their containing file.
// We are trying to get the first statement start potioin, falling back to the program end if statement does not exist
let comments_range_end = program.body.first().map_or(program.span.end, |v| v.span().start);
let comments = ctx.semantic().trivias().comments();
let mut refs_for_import = HashMap::new();
for (start, comment) in comments.range(0..comments_range_end) {
let raw = &ctx.semantic().source_text()[*start as usize..comment.end() as usize];
if let Some((group1, group2)) = get_attr_key_and_value(raw) {
if (group1 == "types" && self.types == TypesOption::Never)
|| (group1 == "path" && self.path == PathOption::Never)
|| (group1 == "lib" && self.lib == LibOption::Never)
{
ctx.diagnostic(TripleSlashReferenceDiagnostic(
group2.to_string(),
Span { start: *start - 2, end: comment.end() },
));
}
if group1 == "types" && self.types == TypesOption::PreferImport {
refs_for_import.insert(group2, Span { start: *start - 2, end: comment.end() });
}
}
}
if !refs_for_import.is_empty() {
for stmt in &program.body {
match stmt {
Statement::Declaration(Declaration::TSImportEqualsDeclaration(decl)) => {
match *decl.module_reference {
TSModuleReference::ExternalModuleReference(ref mod_ref) => {
if let Some(v) =
refs_for_import.get(mod_ref.expression.value.as_str())
{
ctx.diagnostic(TripleSlashReferenceDiagnostic(
mod_ref.expression.value.to_string(),
*v,
));
}
}
TSModuleReference::TypeName(_) => {}
}
}
Statement::ModuleDeclaration(st) => {
if let ModuleDeclaration::ImportDeclaration(ref decl) = **st {
if let Some(v) = refs_for_import.get(decl.source.value.as_str()) {
ctx.diagnostic(TripleSlashReferenceDiagnostic(
decl.source.value.to_string(),
*v,
));
}
}
}
_ => {}
}
}
}
}
}
fn get_attr_key_and_value(raw: &str) -> Option<(String, String)> {
if !raw.starts_with('/') {
return None;
}
let reference_start = "<reference ";
let reference_end = "/>";
if let Some(start_idx) = raw.find(reference_start) {
// Check if the string contains '/>' after the start index
if let Some(end_idx) = raw[start_idx..].find(reference_end) {
let reference_str = &raw[start_idx + reference_start.len()..start_idx + end_idx];
// Split the string by whitespaces
let parts = reference_str.split_whitespace();
// Filter parts that start with attribute key pattern
let filtered_parts: Vec<&str> = parts
.into_iter()
.filter(|part| {
part.starts_with("types=")
|| part.starts_with("path=")
|| part.starts_with("lib=")
})
.collect();
if let Some(attr) = filtered_parts.first() {
// Split the attribute by '=' to get key and value
let attr_parts: Vec<&str> = attr.split('=').collect();
if attr_parts.len() == 2 {
let key = attr_parts[0].trim().trim_matches('"').to_string();
let value = attr_parts[1].trim_matches('"').trim_end_matches('/').to_string();
return Some((key, value));
}
}
}
}
None
}
#[test]
fn test() {
use crate::tester::Tester;
let pass = vec![
(
r#"
// <reference path="foo" />
// <reference types="bar" />
// <reference lib="baz" />
import * as foo from 'foo';
import * as bar from 'bar';
import * as baz from 'baz';
"#,
Some(serde_json::json!([{ "path": "never", "types": "never", "lib": "never" }])),
),
(
r#"
// <reference path="foo" />
// <reference types="bar" />
// <reference lib="baz" />
import foo = require('foo');
import bar = require('bar');
import baz = require('baz');
"#,
Some(serde_json::json!([{ "path": "never", "types": "never", "lib": "never" }])),
),
(
r#"
/// <reference path="foo" />
/// <reference types="bar" />
/// <reference lib="baz" />
import * as foo from 'foo';
import * as bar from 'bar';
import * as baz from 'baz';
"#,
Some(serde_json::json!([{ "path": "always", "types": "always", "lib": "always" }])),
),
(
r#"
/// <reference path="foo" />
/// <reference types="bar" />
/// <reference lib="baz" />
import foo = require('foo');
import bar = require('bar');
import baz = require('baz');
"#,
Some(serde_json::json!([{ "path": "always", "types": "always", "lib": "always" }])),
),
(
r#"
/// <reference path="foo" />
/// <reference types="bar" />
/// <reference lib="baz" />
import foo = foo;
import bar = bar;
import baz = baz;
"#,
Some(serde_json::json!([{ "path": "always", "types": "always", "lib": "always" }])),
),
(
r#"
/// <reference path="foo" />
/// <reference types="bar" />
/// <reference lib="baz" />
import foo = foo.foo;
import bar = bar.bar.bar.bar;
import baz = baz.baz;
"#,
Some(serde_json::json!([{ "path": "always", "types": "always", "lib": "always" }])),
),
(r"import * as foo from 'foo';", Some(serde_json::json!([{ "path": "never" }]))),
(r"import foo = require('foo');", Some(serde_json::json!([{ "path": "never" }]))),
(r"import * as foo from 'foo';", Some(serde_json::json!([{ "types": "never" }]))),
(r"import foo = require('foo');", Some(serde_json::json!([{ "types": "never" }]))),
(r"import * as foo from 'foo';", Some(serde_json::json!([{ "lib": "never" }]))),
(r"import foo = require('foo');", Some(serde_json::json!([{ "lib": "never" }]))),
(r"import * as foo from 'foo';", Some(serde_json::json!([{ "types": "prefer-import" }]))),
(r"import foo = require('foo');", Some(serde_json::json!([{ "types": "prefer-import" }]))),
(
r#"
/// <reference types="foo" />
import * as bar from 'bar';
"#,
Some(serde_json::json!([{ "types": "prefer-import" }])),
),
(
r#"
/*
/// <reference types="foo" />
*/
import * as foo from 'foo';
"#,
Some(serde_json::json!([{ "path": "never", "types": "never", "lib": "never" }])),
),
];
let fail = vec![
(
r#"
/// <reference types="foo" />
import * as foo from 'foo';
"#,
Some(serde_json::json!([{ "types": "prefer-import" }])),
),
(
r#"
/// <reference types="foo" />
import foo = require('foo');
"#,
Some(serde_json::json!([{ "types": "prefer-import" }])),
),
(r#"/// <reference path="foo" />"#, Some(serde_json::json!([{ "path": "never" }]))),
(r#"/// <reference types="foo" />"#, Some(serde_json::json!([{ "types": "never" }]))),
(r#"/// <reference lib="foo" />"#, Some(serde_json::json!([{ "lib": "never" }]))),
];
Tester::new(TripleSlashReference::NAME, pass, fail).test_and_snapshot();
}

View file

@ -0,0 +1,44 @@
---
source: crates/oxc_linter/src/tester.rs
expression: triple_slash_reference
---
⚠ typescript-eslint(triple-slash-reference): Do not use a triple slash reference for foo, use `import` style instead.
╭─[triple_slash_reference.tsx:1:1]
1 │
2 │ /// <reference types="foo" />
· ─────────────────────────────
3 │ import * as foo from 'foo';
╰────
help: Use of triple-slash reference type directives is generally discouraged in favor of ECMAScript Module imports.
⚠ typescript-eslint(triple-slash-reference): Do not use a triple slash reference for foo, use `import` style instead.
╭─[triple_slash_reference.tsx:1:1]
1 │
2 │ /// <reference types="foo" />
· ─────────────────────────────
3 │ import foo = require('foo');
╰────
help: Use of triple-slash reference type directives is generally discouraged in favor of ECMAScript Module imports.
⚠ typescript-eslint(triple-slash-reference): Do not use a triple slash reference for foo, use `import` style instead.
╭─[triple_slash_reference.tsx:1:1]
1 │ /// <reference path="foo" />
· ────────────────────────────
╰────
help: Use of triple-slash reference type directives is generally discouraged in favor of ECMAScript Module imports.
⚠ typescript-eslint(triple-slash-reference): Do not use a triple slash reference for foo, use `import` style instead.
╭─[triple_slash_reference.tsx:1:1]
1 │ /// <reference types="foo" />
· ─────────────────────────────
╰────
help: Use of triple-slash reference type directives is generally discouraged in favor of ECMAScript Module imports.
⚠ typescript-eslint(triple-slash-reference): Do not use a triple slash reference for foo, use `import` style instead.
╭─[triple_slash_reference.tsx:1:1]
1 │ /// <reference lib="foo" />
· ───────────────────────────
╰────
help: Use of triple-slash reference type directives is generally discouraged in favor of ECMAScript Module imports.