mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 12:19:15 +00:00
feat(linter): implement @typescript-eslint/triple-slash-reference (#1903)
implement @typescript-eslint/triple-slash-reference Related issue: https://github.com/oxc-project/oxc/issues/503 original - doc: https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/triple-slash-reference.md - code: https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/src/rules/triple-slash-reference.ts
This commit is contained in:
parent
d4acd140ad
commit
c5887bcb2b
3 changed files with 373 additions and 0 deletions
|
|
@ -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,
|
||||
|
|
|
|||
327
crates/oxc_linter/src/rules/typescript/triple_slash_reference.rs
Normal file
327
crates/oxc_linter/src/rules/typescript/triple_slash_reference.rs
Normal 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();
|
||||
}
|
||||
44
crates/oxc_linter/src/snapshots/triple_slash_reference.snap
Normal file
44
crates/oxc_linter/src/snapshots/triple_slash_reference.snap
Normal 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.
|
||||
|
||||
|
||||
Loading…
Reference in a new issue