Skip to main content

drep/languages/definitions/
javascript.rs

1//! The JavaScript/TypeScript ecosystem, plus the component frameworks that
2//! reuse eslint. Vue and Svelte ship checkers of their own (vue-tsc,
3//! svelte-check), but those are type and language tooling rather than a lint
4//! CLI a project configures the way it configures eslint - so drep checks
5//! `.vue`/`.svelte` files through the project's eslint config, which is where
6//! the parser plugins live when the project has them.
7
8use crate::languages::spec::{
9    DEFAULT_TOOL_TIMEOUT_SECS, DiagnosticsStream, LanguageSupport, OutputFormat, ToolSpec,
10};
11
12/// Package installs and build outputs shared by the JavaScript ecosystem.
13/// npm and its rivals install into `node_modules`; Next.js and Nuxt write
14/// `.next` and `.nuxt` as their build directories. Declared once rather than
15/// repeated across four entries that can never legitimately disagree.
16static JS_VENDORED_DIRS: &[&str] = &["node_modules", ".next", ".nuxt"];
17
18/// JavaScript deterministic checker.
19pub static ESLINT: ToolSpec = ToolSpec {
20    name: "eslint",
21    command: &["eslint", "--format", "json"],
22    local_paths: &["node_modules/.bin/eslint"],
23    config_files: &[
24        "eslint.config.js",
25        "eslint.config.mjs",
26        "eslint.config.cjs",
27        ".eslintrc",
28        ".eslintrc.js",
29        ".eslintrc.cjs",
30        ".eslintrc.json",
31        ".eslintrc.yml",
32        ".eslintrc.yaml",
33    ],
34    config_flag: None,
35    output_format: OutputFormat::Json,
36    diagnostics_stream: DiagnosticsStream::Stdout,
37    timeout_secs: DEFAULT_TOOL_TIMEOUT_SECS,
38    timeout_context: None,
39    establishes_compilation: false,
40    serial_in_repository: false,
41    accepts_files: true,
42};
43
44/// TypeScript's compiler-as-checker. Streams diagnostics to stdout.
45pub static TSC: ToolSpec = ToolSpec {
46    name: "tsc",
47    command: &["tsc", "--noEmit", "--pretty", "false"],
48    local_paths: &["node_modules/.bin/tsc"],
49    config_files: &["tsconfig.json"],
50    config_flag: None,
51    output_format: OutputFormat::Tsc,
52    diagnostics_stream: DiagnosticsStream::Stdout,
53    timeout_secs: DEFAULT_TOOL_TIMEOUT_SECS,
54    timeout_context: None,
55    establishes_compilation: true,
56    serial_in_repository: false,
57    // Passing source files makes tsc ignore tsconfig.json. Run the configured
58    // project and filter its diagnostics back to the requested files.
59    accepts_files: false,
60};
61
62/// JavaScript language entry.
63pub static JAVASCRIPT: LanguageSupport = LanguageSupport {
64    name: "javascript",
65    display_name: "JavaScript",
66    extensions: &[".js", ".jsx", ".mjs", ".cjs"],
67    filenames: &[],
68    filename_prefixes: &[],
69    tools: &[&ESLINT],
70    conventions: &[
71        "Unhandled promise rejections and missing await",
72        "Sequential awaits in a loop where the work is independent",
73        "var versus let/const, and accidental global scope",
74        "Equality coercion (== versus ===)",
75    ],
76    vendored_dirs: JS_VENDORED_DIRS,
77};
78
79/// TypeScript language entry.
80pub static TYPESCRIPT: LanguageSupport = LanguageSupport {
81    name: "typescript",
82    display_name: "TypeScript",
83    extensions: &[".ts", ".tsx", ".mts", ".cts"],
84    filenames: &[],
85    filename_prefixes: &[],
86    tools: &[&ESLINT, &TSC],
87    conventions: &[
88        "`any` where a real type is available, and unsafe casts",
89        "Unhandled promise rejections and missing await",
90        "Non-null assertions (!) that hide a genuine null case",
91        "Sequential awaits in a loop where the work is independent",
92    ],
93    vendored_dirs: JS_VENDORED_DIRS,
94};
95
96/// Vue language entry.
97///
98/// Reuses the project's eslint: a `.vue` file is script plus template, and
99/// eslint with the Vue plugin is the lint setup a Vue project configures.
100/// Without the plugin, eslint reports a parse error on the template - a
101/// finding that is drep's invocation's fault rather than the file's, which
102/// is the price of checking through a config the project may not have
103/// extended to these files.
104pub static VUE: LanguageSupport = LanguageSupport {
105    name: "vue",
106    display_name: "Vue",
107    extensions: &[".vue"],
108    filenames: &[],
109    filename_prefixes: &[],
110    tools: &[&ESLINT],
111    conventions: &[
112        "Reactive state mutated from outside the component that owns it",
113        "v-for without a stable key, and keys that identify the wrong row",
114        "Watchers whose side effects retrigger themselves",
115        "Props mutated directly instead of emitted as events",
116        "Computed properties hiding side effects",
117    ],
118    vendored_dirs: JS_VENDORED_DIRS,
119};
120
121/// Svelte language entry.
122///
123/// Same arrangement as Vue: eslint with the Svelte plugin is the
124/// project-configured lint setup, and without the plugin eslint's parse
125/// error on the template blocks the gate for an invocation reason, not a
126/// code one.
127pub static SVELTE: LanguageSupport = LanguageSupport {
128    name: "svelte",
129    display_name: "Svelte",
130    extensions: &[".svelte"],
131    filenames: &[],
132    filename_prefixes: &[],
133    tools: &[&ESLINT],
134    conventions: &[
135        "Reactive statements with dependencies they do not declare",
136        "Stores subscribed but never unsubscribed on destroy",
137        "State mutated from outside the component tree",
138        "Derived values recomputed on unrelated changes",
139    ],
140    vendored_dirs: JS_VENDORED_DIRS,
141};
142
143/// The family's entries in registration order. See `ALL_LANGUAGES`.
144pub(crate) static FAMILY: &[&LanguageSupport] = &[&JAVASCRIPT, &TYPESCRIPT, &VUE, &SVELTE];