Skip to content

Commit

Permalink
feat(linter): implement @typescript-eslint/triple-slash-reference (#1903
Browse files Browse the repository at this point in the history
  • Loading branch information
kaykdm authored Jan 11, 2024
1 parent d4acd14 commit c5887bc
Show file tree
Hide file tree
Showing 3 changed files with 373 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
327 changes: 327 additions & 0 deletions crates/oxc_linter/src/rules/typescript/triple_slash_reference.rs
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 crates/oxc_linter/src/snapshots/triple_slash_reference.snap
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" />
· ─────────────────────────────
3import * 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.

0 comments on commit c5887bc

Please sign in to comment.