Skip to content
Closed
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
19 changes: 19 additions & 0 deletions .changeset/petite-waves-love.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@biomejs/biome": patch
---

The resolver can now correctly resolve `.ts`, `.tsx`, `.d.ts`, `.js` files by `.js` extension if exists, based on [the file extension substitution in TypeScript](https://www.typescriptlang.org/docs/handbook/modules/reference.html#file-extension-substitution), if the `moduleResolution` option is set to `Node16` or `NodeNext`.

For example, the linter can now detect the floating promise in the following situation, if you have enabled the `noFloatingPromises` rule.

**`foo.ts`**
```ts
export async function doSomething(): Promise<void> {}
```

**`bar.ts`**
```ts
import { doSomething } from "./foo.js"; // doesn't exist actually, but it is resolved to `foo.ts`

doSomething(); // floating promise!
```
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 25 additions & 3 deletions crates/biome_deserialize_macros/src/deserializable_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ use self::container_attrs::{ContainerAttrs, UnknownFields};
use self::struct_field_attrs::DeprecatedField;
use crate::deserializable_derive::enum_variant_attrs::EnumVariantAttrs;
use crate::deserializable_derive::struct_field_attrs::StructFieldAttrs;
use biome_string_case::Case;
use biome_string_case::{Case, StrLikeExtension};
use proc_macro_error2::*;
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use std::borrow::Cow;
use syn::{Data, GenericParam, Generics, Path, Type};

pub(crate) struct DeriveInput {
Expand Down Expand Up @@ -66,6 +67,7 @@ impl DeriveInput {
.collect();
DeserializableData::Enum(DeserializableEnumData {
variants,
case_insensitive: attrs.case_insensitive,
with_validator: attrs.with_validator,
})
}
Expand Down Expand Up @@ -167,6 +169,7 @@ pub enum DeserializableData {
#[derive(Debug)]
pub struct DeserializableEnumData {
variants: Vec<DeserializableVariantData>,
case_insensitive: bool,
with_validator: bool,
}

Expand Down Expand Up @@ -240,7 +243,14 @@ fn generate_deserializable_enum(
let allowed_variants: Vec<_> = data
.variants
.iter()
.map(|DeserializableVariantData { key, .. }| quote! { #key })
.map(|DeserializableVariantData { key, .. }| {
let key = if data.case_insensitive {
key.to_ascii_lowercase_cow()
} else {
Cow::Borrowed(key.as_str())
};
quote! { #key }
})
.collect();

let deserialize_variants: Vec<_> = data
Expand All @@ -251,11 +261,22 @@ fn generate_deserializable_enum(
ident: variant_ident,
key,
}| {
let key = if data.case_insensitive {
key.to_ascii_lowercase_cow()
} else {
Cow::Borrowed(key.as_str())
};
quote! { #key => Self::#variant_ident }
},
)
.collect();

let discriminant = if data.case_insensitive {
quote! { biome_string_case::StrLikeExtension::to_ascii_lowercase_cow(text.text()).as_ref() }
} else {
quote! { text.text() }
};

let validator = if data.with_validator {
quote! {
if !biome_deserialize::DeserializableValidator::validate(&mut result, ctx, name, value.range()) {
Expand All @@ -275,7 +296,8 @@ fn generate_deserializable_enum(
value: &impl biome_deserialize::DeserializableValue,
name: &str,
) -> Option<Self> {
let mut result = match biome_deserialize::Text::deserialize(ctx, value, name)?.text() {
let text = biome_deserialize::Text::deserialize(ctx, value, name)?;
let mut result = match #discriminant {
#(#deserialize_variants),*,
unknown_variant => {
const ALLOWED_VARIANTS: &[&str] = &[#(#allowed_variants),*];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub(crate) struct ContainerAttrs {
pub try_from: Option<Path>,
/// Ignore unknown fields in a struct upon deserialization.
pub unknown_fields: Option<UnknownFields>,
/// Allow case-insensitive match for enum variants. Only supported in enums.
pub case_insensitive: bool,
}

/// Attributes for struct that control how unkinown fields are handled.
Expand Down Expand Up @@ -74,6 +76,9 @@ impl TryFrom<&Vec<Attribute>> for ContainerAttrs {
Err(error) => return Err(Error::new(meta.span(), error)),
}
}
Meta::Path(path) if path.is_ident("case_insensitive") => {
opts.case_insensitive = true
}
_ => {
let meta_str = meta.to_token_stream().to_string();
return Err(Error::new(
Expand Down
11 changes: 10 additions & 1 deletion crates/biome_module_graph/src/js_module_info/visitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ use crate::{

use super::{ResolvedPath, collector::JsModuleInfoCollector};

/// Extensions to try to resolve based on the extension in the import specifier.
/// ref: https://www.typescriptlang.org/docs/handbook/modules/reference.html#the-moduleresolution-compiler-option
const EXTENSION_ALIASES: &[(&str, &[&str])] = &[
("js", &["ts", "tsx", "d.ts", "jsx"]),
("mjs", &["mts", "d.mts"]),
("cjs", &["cts", "d.cts"]),
];

pub(crate) struct JsModuleVisitor<'a> {
root: AnyJsRoot,
directory: &'a Utf8Path,
Expand Down Expand Up @@ -393,7 +401,7 @@ impl<'a> JsModuleVisitor<'a> {
if let Ok(binding) = node.pattern() {
self.visit_binding_pattern(
binding,
collector,
collector,
);
}
}
Expand Down Expand Up @@ -442,6 +450,7 @@ impl<'a> JsModuleVisitor<'a> {
condition_names: &["types", "import", "default"],
default_files: &["index"],
extensions: SUPPORTED_EXTENSIONS,
extension_aliases: EXTENSION_ALIASES,
resolve_node_builtins: true,
resolve_types: true,
..Default::default()
Expand Down
1 change: 1 addition & 0 deletions crates/biome_package/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ biome_json_syntax = { workspace = true }
biome_json_value = { workspace = true }
biome_parser = { workspace = true }
biome_rowan = { workspace = true }
biome_string_case = { workspace = true }
biome_text_size = { workspace = true }
camino = { workspace = true }
indexmap = { workspace = true }
Expand Down
3 changes: 2 additions & 1 deletion crates/biome_package/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ use biome_fs::FileSystem;
use camino::Utf8Path;
pub use license::generated::*;
pub use node_js_package::{
CompilerOptions, Dependencies, NodeJsPackage, PackageJson, PackageType, TsConfigJson, Version,
CompilerOptions, Dependencies, Module, ModuleResolution, NodeJsPackage, PackageJson,
PackageType, TsConfigJson, Version,
};

use std::any::TypeId;
Expand Down
2 changes: 1 addition & 1 deletion crates/biome_package/src/node_js_package/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ mod tsconfig_json;

use camino::Utf8Path;
pub use package_json::{Dependencies, PackageJson, PackageType, Version};
pub use tsconfig_json::{CompilerOptions, TsConfigJson};
pub use tsconfig_json::{CompilerOptions, Module, ModuleResolution, TsConfigJson};

use biome_rowan::Language;

Expand Down
40 changes: 40 additions & 0 deletions crates/biome_package/src/node_js_package/tsconfig_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,50 @@ pub struct CompilerOptions {
/// See: https://www.typescriptlang.org/tsconfig/#typeRoots
#[deserializable(rename = "typeRoots")]
pub type_roots: Option<Vec<String>>,

/// Sets the module system for the program.
/// https://www.typescriptlang.org/tsconfig/#module
pub module: Option<Module>,

/// Specify the module resolution strategy.
/// https://www.typescriptlang.org/tsconfig/#moduleResolution
#[deserializable(rename = "moduleResolution")]
pub module_resolution: Option<ModuleResolution>,
}

pub type CompilerOptionsPathsMap = IndexMap<String, Vec<String>, BuildHasherDefault<FxHasher>>;

#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserializable)]
#[deserializable(case_insensitive)]
pub enum Module {
None,
CommonJS,
Amd,
Umd,
System,
ES6,
ES2015,
ES2020,
ES2022,
ESNext,
Node16,
Node18,
Node20,
NodeNext,
Preserve,
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserializable)]
#[deserializable(case_insensitive)]
pub enum ModuleResolution {
Classic,
Node,
Node10,
Node16,
NodeNext,
Bundler,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ExtendsField {
Single(String),
Expand Down
Loading
Loading