Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 115 additions & 2 deletions crates/oxc_linter/src/frameworks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,119 @@ pub fn has_jest_imports(module_record: &ModuleRecord) -> bool {
#[derive(Debug, Clone, Copy, Eq, PartialEq)]

pub enum FrameworkOptions {
Default, // default
VueSetup, // context is inside `<script setup>`
Default, // default
VueSetup, // context is inside `<script setup>`
SvelteModule, // `<script module>`
Svelte, // `<script>`
AstroFrontmatter, // within `---`-delimited block
}

/// Vue 3 compiler macros available in `<script setup>`
/// Reference: <https://github.com/vuejs/vue-eslint-parser/blob/5ff1a4fda76b07608cc17687a976c2309f5648e2/src/script-setup/scope-analyzer.ts#L86>
static VUE_SETUP_GLOBALS: [&str; 7] = [
"defineProps",
"defineEmits",
"defineExpose",
"withDefaults",
"defineOptions",
"defineSlots",
"defineModel",
];

/// Svelte runes available in `<script>` context
/// Reference: <https://github.com/sveltejs/svelte/blob/da00abe1162a8e56455e92b79020c4e33290e10e/packages/svelte/src/ambient.d.ts#L23>
static SVELTE_GLOBALS: [&str; 7] =
["$state", "$derived", "$effect", "$props", "$bindable", "$inspect", "$host"];

/// A subset of Svelte runes is available in `<script module>` context.
static SVELTE_MODULE_GLOBALS: [&str; 4] = ["$state", "$derived", "$effect", "$inspect"];

impl FrameworkOptions {
/// Check if a variable is a framework-specific global in this context.
///
/// Returns `true` if the variable is a framework global, `false` otherwise.
/// Framework globals are always readonly, as they are compiler macros or
/// special identifiers provided by the framework.
///
/// # Examples
/// ```
/// // In a Vue <script setup> context
/// let options = FrameworkOptions::VueSetup;
/// assert!(options.has_global("defineProps") == true);
/// assert!(options.has_global("defineEmits") == true);
/// assert!(options.has_global("console") == false);
///
/// // No framework globals by default
/// let options = FrameworkOptions::Default;
/// assert!(options.has_global("defineProps") == false);
/// assert!(options.has_global("console") == false);
/// ```
pub fn has_global(self, var: &str) -> bool {
match self {
Self::Default => false,
Self::VueSetup => VUE_SETUP_GLOBALS.contains(&var),
Self::SvelteModule => SVELTE_MODULE_GLOBALS.contains(&var),
Self::Svelte => SVELTE_GLOBALS.contains(&var),
// All of Astro's utilities are grouped under the `Astro` namespace.
Self::AstroFrontmatter => var == "Astro",
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_vue_setup_globals() {
// Test all Vue compiler macros
let options = FrameworkOptions::VueSetup;
assert!(options.has_global("defineProps"));
assert!(options.has_global("defineEmits"));
assert!(options.has_global("defineExpose"));
assert!(options.has_global("defineOptions"));
assert!(options.has_global("defineSlots"));
assert!(options.has_global("defineModel"));
assert!(options.has_global("withDefaults"));

// Test that non-Vue globals are not included
assert!(!options.has_global("console"));
assert!(!options.has_global("window"));
assert!(!options.has_global("randomVariable"));
}

#[test]
fn test_svelte_globals() {
let options = FrameworkOptions::Svelte;
assert!(options.has_global("$state"));
assert!(options.has_global("$derived"));
assert!(options.has_global("$effect"));
assert!(options.has_global("$props"));
assert!(options.has_global("$bindable"));
assert!(options.has_global("$inspect"));
assert!(options.has_global("$host"));

let globals = FrameworkOptions::SvelteModule;
assert!(globals.has_global("$state"));
assert!(globals.has_global("$derived"));
assert!(globals.has_global("$effect"));
assert!(globals.has_global("$inspect"));
assert!(!globals.has_global("$props"));
assert!(!globals.has_global("$bindable"));
assert!(!globals.has_global("$host"));
}

#[test]
fn test_astro_frontmatter_globals() {
let options = FrameworkOptions::AstroFrontmatter;
assert!(options.has_global("Astro"));
}

#[test]
fn test_default_no_globals() {
// Default context has no framework globals
let options = FrameworkOptions::Default;
assert!(!options.has_global("defineProps"));
assert!(!options.has_global("console"));
}
}
9 changes: 7 additions & 2 deletions crates/oxc_linter/src/loader/partial_loader/astro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use memchr::memmem::Finder;

use oxc_span::{SourceType, Span};

use crate::loader::JavaScriptSource;
use crate::{frameworks::FrameworkOptions, loader::JavaScriptSource};

use super::{SCRIPT_END, SCRIPT_START};

Expand Down Expand Up @@ -47,7 +47,12 @@ impl<'a> AstroPartialLoader<'a> {
// move start to the end of the ASTRO_SPLIT
let start = start + ASTRO_SPLIT.len() as u32;
let js_code = Span::new(start, end).source_text(self.source_text);
Some(JavaScriptSource::partial(js_code, SourceType::ts(), start))
Some(JavaScriptSource::partial_with_framework_options(
js_code,
SourceType::ts(),
FrameworkOptions::AstroFrontmatter,
start,
))
}

/// In .astro files, you can add client-side JavaScript by adding one (or more) `<script>` tags.
Expand Down
10 changes: 8 additions & 2 deletions crates/oxc_linter/src/loader/partial_loader/svelte.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use memchr::memmem::Finder;

use oxc_span::SourceType;

use crate::loader::JavaScriptSource;
use crate::{frameworks::FrameworkOptions, loader::JavaScriptSource};

use super::{SCRIPT_END, SCRIPT_START, find_script_closing_angle};

Expand Down Expand Up @@ -48,6 +48,7 @@ impl<'a> SveltePartialLoader<'a> {
// get lang="ts" attribute
let content = &self.source_text[*pointer..*pointer + offset];
let is_ts = content.contains("ts");
let is_module = content.contains("module");

*pointer += offset + 1;
let js_start = *pointer;
Expand All @@ -62,7 +63,12 @@ impl<'a> SveltePartialLoader<'a> {

// NOTE: loader checked that source_text.len() is less than u32::MAX
#[expect(clippy::cast_possible_truncation)]
Some(JavaScriptSource::partial(source_text, source_type, js_start as u32))
Some(JavaScriptSource::partial_with_framework_options(
source_text,
source_type,
if is_module { FrameworkOptions::SvelteModule } else { FrameworkOptions::Svelte },
js_start as u32,
))
}
}

Expand Down
4 changes: 3 additions & 1 deletion crates/oxc_linter/src/rules/eslint/no_redeclare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ impl Rule for NoRedeclare {
let name = ctx.scoping().symbol_name(symbol_id);
let decl_span = ctx.scoping().symbol_span(symbol_id);
let is_builtin = self.built_in_globals
&& (GLOBALS["builtin"].contains_key(name) || ctx.globals().is_enabled(name));
&& (GLOBALS["builtin"].contains_key(name)
|| ctx.globals().is_enabled(name)
|| ctx.frameworks_options().has_global(name));

if is_builtin {
ctx.diagnostic(no_redeclare_as_builtin_in_diagnostic(name, decl_span));
Expand Down
5 changes: 5 additions & 0 deletions crates/oxc_linter/src/rules/eslint/no_undef.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ impl Rule for NoUndef {
continue;
}

// Check framework-specific globals (e.g., Vue's defineProps in <script setup>)
if ctx.frameworks_options().has_global(name) {
continue;
}

// Skip reporting error for 'arguments' if it's in a function scope
if name == "arguments"
&& ctx
Expand Down
Loading