Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/aliased-antilopes-acknowledge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

Fixed [#7111](https://github.com/biomejs/biome/issues/7111): Imported symbols using aliases are now correctly recognised.
10 changes: 4 additions & 6 deletions crates/biome_js_analyze/tests/quick_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,20 @@ fn project_layout_with_top_level_dependencies(dependencies: Dependencies) -> Arc
#[test]
fn quick_test() {
const FILENAME: &str = "dummyFile.ts";
const SOURCE: &str = r#"function head<T>(items: T[]) {
if (items) { // This check is unnecessary
return items[0].toUpperCase();
}
}"#;
const SOURCE: &str = r#"import { sleep as alias } from "./sleep.ts";
alias(100);"#;

let parsed = parse(SOURCE, JsFileSource::tsx(), JsParserOptions::default());

let mut fs = TemporaryFs::new("quick_test");
fs.create_file("sleep.ts", "export const sleep = async (ms = 1000): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));");
fs.create_file(FILENAME, SOURCE);

let file_path = Utf8PathBuf::from(format!("{}/{FILENAME}", fs.cli_path()));

let mut error_ranges: Vec<TextRange> = Vec::new();
let options = AnalyzerOptions::default().with_file_path(file_path.clone());
let rule_filter = RuleFilter::Rule("nursery", "noUnnecessaryConditions");
let rule_filter = RuleFilter::Rule("nursery", "noFloatingPromises");

let dependencies = Dependencies(Box::new([("buffer".into(), "latest".into())]));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { returnPromiseResult } from "./returnPromiseResult.ts";
import { returnPromiseResult as returnAliasedPromiseResult } from "./returnPromiseResult.ts";

async function returnsPromise(): Promise<string> {
return "value";
Expand Down Expand Up @@ -335,6 +336,7 @@ async function testDestructuringAndCallingReturnsPromiseFromRest({
import("some-module").then(() => {});

returnPromiseResult();
returnAliasedPromiseResult();

function returnMaybePromise(): Promise<void> | undefined {
if (!false) {
Expand Down

Large diffs are not rendered by default.

14 changes: 8 additions & 6 deletions crates/biome_js_syntax/src/import_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,15 @@ impl AnyJsNamedImportSpecifier {
/// ```
pub fn imported_name(&self) -> Option<JsSyntaxToken> {
match self {
specifier @ (Self::JsNamedImportSpecifier(_)
| Self::JsShorthandNamedImportSpecifier(_)) => specifier
.local_name()?
.as_js_identifier_binding()?
.name_token()
.ok(),
Self::JsBogusNamedImportSpecifier(_) => None,
Self::JsNamedImportSpecifier(specifier) => {
specifier.name().and_then(|name| name.value()).ok()
}
Self::JsShorthandNamedImportSpecifier(specifier) => {
let imported_name = specifier.local_name().ok()?;
let imported_name = imported_name.as_js_identifier_binding()?;
imported_name.name_token().ok()
}
}
}

Expand Down
107 changes: 68 additions & 39 deletions crates/biome_resolver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,17 +192,12 @@ fn resolve_module_with_package_json(

// Initialise `type_roots` from the `tsconfig.json` if we have one.
let initialise_type_roots = options.resolve_types && options.type_roots.is_auto();
let type_roots: Option<Vec<&str>> = match initialise_type_roots {
true => tsconfig
.as_ref()
.ok()
.and_then(|tsconfig| tsconfig.compiler_options.type_roots.as_ref())
.map(|type_roots| type_roots.iter().map(String::as_str).collect()),
false => None,
};
let options = match initialise_type_roots {
true => &options.with_type_roots_and_without_manifests(TypeRoots::from_optional_slice(
type_roots.as_deref(),
tsconfig
.as_ref()
.ok()
.and_then(|tsconfig| tsconfig.compiler_options.type_roots.as_deref()),
)),
false => options,
};
Expand Down Expand Up @@ -460,35 +455,32 @@ fn resolve_dependency(
) -> Result<Utf8PathBuf, ResolveError> {
let (package_name, subpath) = parse_package_specifier(specifier)?;

if let TypeRoots::Explicit(type_roots) = options.type_roots {
for type_root in type_roots {
let package_path = base_dir.join(type_root).join(package_name);
match resolve_package_path(&package_path, subpath, fs, options) {
Ok(path) => return Ok(path),
Err(ResolveError::NotFound) => { /* continue */ }
Err(error) => return Err(error),
}
for type_root in options.type_roots.explicit_roots() {
let package_path = base_dir.join(type_root).join(package_name);
match resolve_package_path(&package_path, subpath, fs, options) {
Ok(path) => return Ok(path),
Err(ResolveError::NotFound) => { /* continue */ }
Err(error) => return Err(error),
}

// FIXME: This is an incomplete approximation of how resolving
// inside custom `typeRoots` should work. Besides packages,
// type roots may contain individual `d.ts` files. Such files
// don't even need to match the name of the package, because
// they can do things such as
// `declare module "whatever_package_name"`. But to get these
// things to work reliably, we need to track **global** type
// definitions first, so for now we'll assume a correlation
// between package name and module name.
for extension in options.extensions {
if let Some(extension) = definition_extension_for_js_extension(extension) {
let path = package_path.with_extension(extension);
match fs.path_info(&path) {
Ok(PathInfo::File) => return Ok(normalize_path(&path)),
Ok(PathInfo::Symlink {
canonicalized_target,
}) => return Ok(canonicalized_target),
_ => { /* continue */ }
};
}
// FIXME: This is an incomplete approximation of how resolving inside
// custom `typeRoots` should work. Besides packages, type roots
// may contain individual `d.ts` files. Such files don't even
// need to match the name of the package, because they can do
// things such as `declare module "whatever_package_name"`. But
// to get these things to work reliably, we need to track
// **global** type definitions first, so for now we'll assume a
// correlation between package name and module name.
for extension in options.extensions {
if let Some(extension) = definition_extension_for_js_extension(extension) {
let path = package_path.with_extension(extension);
match fs.path_info(&path) {
Ok(PathInfo::File) => return Ok(normalize_path(&path)),
Ok(PathInfo::Symlink {
canonicalized_target,
}) => return Ok(canonicalized_target),
_ => { /* continue */ }
};
}
}
}
Expand Down Expand Up @@ -935,6 +927,12 @@ pub enum TypeRoots<'a> {
/// Relative paths are resolved from the package path.
Explicit(&'a [&'a str]),

/// Explicit list of directories to search.
///
/// Same as [`TypeRoots::Explicit`] except it references a slice of owned
/// strings.
ExplicitOwned(&'a [String]),

/// The default value to use if no `compilerOptions.typeRoots` field can be
/// found in the `tsconfig.json`.
///
Expand All @@ -944,18 +942,49 @@ pub enum TypeRoots<'a> {
}

impl<'a> TypeRoots<'a> {
const fn from_optional_slice(type_roots: Option<&'a [&'a str]>) -> Self {
const fn from_optional_slice(type_roots: Option<&'a [String]>) -> Self {
match type_roots {
Some(type_roots) => Self::Explicit(type_roots),
Some(type_roots) => Self::ExplicitOwned(type_roots),
None => Self::TypesInNodeModules,
}
}

fn explicit_roots(&self) -> impl Iterator<Item = &str> {
ExplicitTypeRootIterator {
type_roots: self,
index: 0,
}
}

const fn is_auto(self) -> bool {
matches!(self, Self::Auto)
}
}

struct ExplicitTypeRootIterator<'a> {
type_roots: &'a TypeRoots<'a>,
index: usize,
}

impl<'a> Iterator for ExplicitTypeRootIterator<'a> {
type Item = &'a str;

fn next(&mut self) -> Option<Self::Item> {
match self.type_roots {
TypeRoots::Auto => None,
TypeRoots::Explicit(items) => items.get(self.index).map(|root| {
self.index += 1;
*root
}),
TypeRoots::ExplicitOwned(items) => items.get(self.index).map(|root| {
self.index += 1;
root.as_str()
}),
TypeRoots::TypesInNodeModules => None,
}
}
}

/// Reference-counted resolved path wrapped in a [Result] that may contain an
/// error if the resolution failed.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
Expand Down

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

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

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

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

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

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

35 changes: 35 additions & 0 deletions crates/biome_resolver/tests/spec_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,3 +498,38 @@ fn test_resolve_type_definitions_with_custom_type_roots() {
)))
);
}

#[test]
fn test_resolve_type_definitions_without_type_specification() {
let base_dir = get_fixtures_path("resolver_cases_5");
let fs = OsFileSystem::new(base_dir.clone());

let options = ResolveOptions {
condition_names: &["types", "import", "default"],
default_files: &["index"],
extensions: &["ts", "js"],
resolve_types: true,
type_roots: TypeRoots::Auto, // auto-detected from `tsconfig.json`
..Default::default()
};

assert_eq!(
resolve("sleep", &base_dir, &fs, &options),
Ok(Utf8PathBuf::from(format!(
"{base_dir}/node_modules/sleep/dist/index.d.ts"
)))
);

// Make sure the re-export is resolvable too:
assert_eq!(
resolve(
"./src/index",
Utf8Path::new(&format!("{base_dir}/node_modules/sleep/dist")),
&fs,
&options
),
Ok(Utf8PathBuf::from(format!(
"{base_dir}/node_modules/sleep/dist/src/index.d.ts"
)))
);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is another bonus. I suspected there was an issue in this area, but there wasn't. At least we have extra coverage now.

Loading