forked from oxc-project/oxc
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(linter): implement @typescript-eslint/triple-slash-reference (ox…
…c-project#1903) implement @typescript-eslint/triple-slash-reference Related issue: oxc-project#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
- Loading branch information
1 parent
aa78a4c
commit 2b8551f
Showing
3 changed files
with
373 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
327 changes: 327 additions & 0 deletions
327
crates/oxc_linter/src/rules/typescript/triple_slash_reference.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 changes: 44 additions & 0 deletions
44
crates/oxc_linter/src/snapshots/triple_slash_reference.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||