Skip to content

Commit

Permalink
feat(linter): eslint-plugin-next: no-typos (#1978)
Browse files Browse the repository at this point in the history
  • Loading branch information
kaykdm authored Jan 13, 2024
1 parent ac4b3a4 commit 8f0f824
Show file tree
Hide file tree
Showing 3 changed files with 340 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 @@ -289,6 +289,7 @@ mod nextjs {
pub mod no_script_component_in_head;
pub mod no_sync_scripts;
pub mod no_title_in_document_head;
pub mod no_typos;
}

oxc_macros::declare_all_lint_rules! {
Expand Down Expand Up @@ -547,4 +548,5 @@ oxc_macros::declare_all_lint_rules! {
nextjs::no_script_component_in_head,
nextjs::no_sync_scripts,
nextjs::no_title_in_document_head,
nextjs::no_typos,
}
288 changes: 288 additions & 0 deletions crates/oxc_linter/src/rules/nextjs/no_typos.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
use oxc_ast::{
ast::{BindingPatternKind, Declaration, ModuleDeclaration},
AstKind,
};
use oxc_diagnostics::{
miette::{self, Diagnostic},
thiserror::{self, Error},
};
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;
use phf::phf_set;

use crate::{context::LintContext, rule::Rule, AstNode};

#[derive(Debug, Error, Diagnostic)]
#[error("eslint-plugin-next(no-typos): {0} may be a typo. Did you mean {1}?")]
#[diagnostic(severity(warning), help("Prevent common typos in Next.js's data fetching functions"))]
struct NoTyposDiagnostic(String, String, #[label] pub Span);

#[derive(Debug, Default, Clone)]
pub struct NoTypos;

declare_oxc_lint!(
/// ### What it does
/// Prevent common typos in Next.js's data fetching functions
///
/// ### Why is this bad?
///
///
/// ### Example
/// ```javascript
/// export default function Page() {
/// return <div></div>;
/// }
/// export async function getServurSideProps(){};
/// ```
NoTypos,
correctness
);

const NEXTJS_DATA_FETCHING_FUNCTIONS: phf::Set<&'static str> = phf_set! {
"getStaticProps",
"getStaticPaths",
"getServerSideProps",
};

// 0 is the exact match
const THRESHOLD: i32 = 1;

impl Rule for NoTypos {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let Some(path) = ctx.file_path().to_str() else { return };
let Some(path_after_pages) = path.split("pages").nth(1) else { return };
if path_after_pages.starts_with("/api") {
return;
}
if let AstKind::ModuleDeclaration(ModuleDeclaration::ExportNamedDeclaration(en_decl)) =
node.kind()
{
if let Some(ref decl) = en_decl.declaration {
match decl {
Declaration::VariableDeclaration(decl) => {
for decl in &decl.declarations {
let BindingPatternKind::BindingIdentifier(id) = &decl.id.kind else {
continue;
};
let Some(potential_typo) = get_potential_typo(&id.name) else {
continue;
};
ctx.diagnostic(NoTyposDiagnostic(
id.name.to_string(),
potential_typo.to_string(),
id.span,
));
}
}
Declaration::FunctionDeclaration(decl) => {
let Some(id) = &decl.id else { return };
let Some(potential_typo) = get_potential_typo(&id.name) else { return };
ctx.diagnostic(NoTyposDiagnostic(
id.name.to_string(),
potential_typo.to_string(),
id.span,
));
}
_ => {}
}
}
}
}
}

fn get_potential_typo(fn_name: &str) -> Option<&str> {
let mut potential_typos: Vec<_> = NEXTJS_DATA_FETCHING_FUNCTIONS
.iter()
.map(|&o| {
let distance = min_distance(o, fn_name);
(o, distance)
})
.filter(|&(_, distance)| distance <= THRESHOLD as usize && distance > 0)
.collect();

potential_typos.sort_by(|a, b| a.1.cmp(&b.1));

potential_typos.first().map(|(option, _)| *option)
}

// the minimum number of operations required to convert string a to string b.
fn min_distance(a: &str, b: &str) -> usize {
let m = a.len();
let n = b.len();

if m < n {
return min_distance(b, a);
}

if n == 0 {
return m;
}

let mut previous_row: Vec<usize> = (0..=n).collect();

for (i, s1) in a.chars().enumerate() {
let mut current_row = vec![i + 1];
for (j, s2) in b.chars().enumerate() {
let insertions = previous_row[j + 1] + 1;
let deletions = current_row[j] + 1;
let substitutions = previous_row[j] + usize::from(s1 != s2);
current_row.push(insertions.min(deletions).min(substitutions));
}
previous_row = current_row;
}
previous_row[n]
}

#[test]
fn test() {
use crate::tester::Tester;
use std::path::PathBuf;

let pass = vec![
(
r"
export default function Page() {
return <div></div>;
}
export const getStaticPaths = async () => {};
export const getStaticProps = async () => {};
",
None,
None,
Some(PathBuf::from("pages/test.tsx")),
),
(
r"
export default function Page() {
return <div></div>;
}
export const getServerSideProps = async () => {};
",
None,
None,
Some(PathBuf::from("pages/test.tsx")),
),
(
r"
export default function Page() {
return <div></div>;
}
export async function getStaticPaths() {};
export async function getStaticProps() {};
",
None,
None,
Some(PathBuf::from("pages/test.tsx")),
),
(
r"
export default function Page() {
return <div></div>;
}
export async function getServerSideProps() {};
",
None,
None,
Some(PathBuf::from("pages/test.tsx")),
),
(
r"
export default function Page() {
return <div></div>;
}
export async function getServerSidePropsss() {};
",
None,
None,
Some(PathBuf::from("pages/test.tsx")),
),
(
r"
export default function Page() {
return <div></div>;
}
export async function getstatisPath() {};
",
None,
None,
Some(PathBuf::from("pages/test.tsx")),
),
// even though there is a typo match, this should not fail because a file is not inside pages directory
(
r"
export default function Page() {
return <div></div>;
}
export const getStaticpaths = async () => {};
export const getStaticProps = async () => {};
",
None,
None,
Some(PathBuf::from("test.tsx")),
),
// even though there is a typo match, this should not fail because a file is inside pages/api directory
(
r"
export default function Page() {
return <div></div>;
}
export const getStaticpaths = async () => {};
export const getStaticProps = async () => {};
",
None,
None,
Some(PathBuf::from("pages/api/test.tsx")),
),
];

let fail = vec![
(
r"
export default function Page() {
return <div></div>;
}
export const getStaticpaths = async () => {};
export const getStaticProps = async () => {};
",
None,
None,
Some(PathBuf::from("pages/test.tsx")),
),
(
r"
export default function Page() {
return <div></div>;
}
export async function getStaticPathss(){};
export async function getStaticPropss(){};
",
None,
None,
Some(PathBuf::from("pages/test.tsx")),
),
(
r"
export default function Page() {
return <div></div>;
}
export async function getServurSideProps(){};
",
None,
None,
Some(PathBuf::from("pages/test.tsx")),
),
(
r"
export default function Page() {
return <div></div>;
}
export const getServurSideProps = () => {};
",
None,
None,
Some(PathBuf::from("pages/test.tsx")),
),
];

Tester::new(NoTypos::NAME, pass, fail).test_and_snapshot();
}
50 changes: 50 additions & 0 deletions crates/oxc_linter/src/snapshots/no_typos.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
source: crates/oxc_linter/src/tester.rs
expression: no_typos
---
eslint-plugin-next(no-typos): getStaticpaths may be a typo. Did you mean getStaticPaths?
╭─[no_typos.tsx:4:1]
4 │ }
5export const getStaticpaths = async () => {};
· ──────────────
6export const getStaticProps = async () => {};
╰────
help: Prevent common typos in Next.js's data fetching functions

eslint-plugin-next(no-typos): getStaticPathss may be a typo. Did you mean getStaticPaths?
╭─[no_typos.tsx:4:1]
4 │ }
5export async function getStaticPathss(){};
· ───────────────
6export async function getStaticPropss(){};
╰────
help: Prevent common typos in Next.js's data fetching functions

eslint-plugin-next(no-typos): getStaticPropss may be a typo. Did you mean getStaticProps?
╭─[no_typos.tsx:5:1]
5export async function getStaticPathss(){};
6export async function getStaticPropss(){};
· ───────────────
7
╰────
help: Prevent common typos in Next.js's data fetching functions

eslint-plugin-next(no-typos): getServurSideProps may be a typo. Did you mean getServerSideProps?
╭─[no_typos.tsx:4:1]
4 │ }
5export async function getServurSideProps(){};
· ──────────────────
6
╰────
help: Prevent common typos in Next.js's data fetching functions

eslint-plugin-next(no-typos): getServurSideProps may be a typo. Did you mean getServerSideProps?
╭─[no_typos.tsx:4:1]
4 │ }
5export const getServurSideProps = () => {};
· ──────────────────
6
╰────
help: Prevent common typos in Next.js's data fetching functions


0 comments on commit 8f0f824

Please sign in to comment.