Skip to content

Commit

Permalink
feat: Extend native parser with tsconfig.json paths
Browse files Browse the repository at this point in the history
Essentially just a repaint of nodejs subpath imports, except that
the parser now allows the bare "*", which means "match anything".
  • Loading branch information
tobni committed Jul 16, 2024
1 parent 61d34f6 commit b284ec9
Show file tree
Hide file tree
Showing 9 changed files with 248 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,15 @@ async def prepare_inference_metadata(imports: PackageJsonImports) -> InferenceMe
return InferenceMetadata.javascript(
imports.root_dir,
{pattern: list(replacements) for pattern, replacements in imports.imports.items()},
None,
{}
)


async def _prepare_inference_metadata(address: Address) -> InferenceMetadata:
owning_pkg = await Get(OwningNodePackage, OwningNodePackageRequest(address))
if not owning_pkg.target:
return InferenceMetadata.javascript(address.spec_path, {})
return InferenceMetadata.javascript(address.spec_path, {}, None, {})
return await Get(
InferenceMetadata, PackageJsonSourceField, owning_pkg.target[PackageJsonSourceField]
)
Expand Down
2 changes: 1 addition & 1 deletion src/python/pants/engine/internals/native_engine.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -703,7 +703,7 @@ class PyStubCAS:
class InferenceMetadata:
@staticmethod
def javascript(
package_root: str, import_patterns: dict[str, list[str]]
package_root: str, import_patterns: dict[str, list[str]], config_root: str | None, paths: dict[str, list[str]]
) -> InferenceMetadata: ...
def __eq__(self, other: InferenceMetadata | Any) -> bool: ...
def __hash__(self) -> int: ...
Expand Down
10 changes: 8 additions & 2 deletions src/python/pants/engine/internals/native_engine_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@

def test_can_construct_javascript_metadata() -> None:
InferenceMetadata.javascript(
package_root="some/dir", import_patterns={"a-pattern-*": ["replaces-me-*"]}
package_root="some/dir",
import_patterns={"a-pattern-*": ["replaces-me-*"]},
config_root=None,
paths={},
)


def test_can_construct_native_dependencies_request() -> None:
NativeDependenciesRequest(EMPTY_DIGEST, None)
NativeDependenciesRequest(
EMPTY_DIGEST, InferenceMetadata.javascript(package_root="some/dir", import_patterns={})
EMPTY_DIGEST,
InferenceMetadata.javascript(
package_root="some/dir", import_patterns={}, config_root=None, paths={"src": ("1", "2")}
),
)
46 changes: 37 additions & 9 deletions src/rust/engine/dep_inference/src/javascript/import_pattern.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright 2023 Pants project contributors (see CONTRIBUTORS.md).
// Licensed under the Apache License, Version 2.0 (see LICENSE).
use std::iter::once;
use std::iter::{once, Once};
use std::ops::Deref;
use std::option::Option;
use std::path::Path;

Expand Down Expand Up @@ -49,9 +50,9 @@ impl<'a> Pattern<'a> {
};
match (prefix, suffix) {
(Some(specifier), None) if specifier == import => Self::from_prefix(import),
(None, _) | (Some(""), _) => Self::NoMatch, // "*<postfix>" isn't valid, or pattern is empty string which is also not interesting.
(None, _) => Self::NoMatch, // pattern is empty string.
(Some(prefix), Some("")) => {
// "<prefix>*"
// "<prefix>*", note that a single "*" also matches here.
if let Some(star_match) = import.strip_prefix(prefix) {
Self::from_prefix_match(prefix, star_match)
} else {
Expand All @@ -74,27 +75,54 @@ impl<'a> Pattern<'a> {
}
}

#[derive(Debug, Eq, PartialEq, Hash)]
pub(crate) enum Import {
Matched(String),
UnMatched(String),
}

impl Deref for Import {
type Target = String;

fn deref(&self) -> &Self::Target {
match self {
Self::Matched(string) | Self::UnMatched(string) => string,
}
}
}

impl IntoIterator for Import {
type Item = String;
type IntoIter = Once<String>;

fn into_iter(self) -> Self::IntoIter {
once(match self {
Self::Matched(string) | Self::UnMatched(string) => string,
})
}
}

/// Replaces patterns provided on the form outlined in
/// [NodeJS subpath patterns](https://nodejs.org/api/packages.html#subpath-patterns).
/// If no pattern matches, the import string is returned unchanged.
pub fn imports_from_patterns(
root: &str,
patterns: &HashMap<String, Vec<String>>,
import: String,
) -> HashSet<String> {
if let Some((star_match, pattern)) = find_best_match(patterns, &import) {
import: &str,
) -> HashSet<Import> {
if let Some((star_match, pattern)) = find_best_match(patterns, import) {
let mut matches = patterns[pattern]
.iter()
.filter_map(move |replacement| apply_replacements_to_match(&star_match, replacement))
.map(|new_import| add_root_dir_to_dot_slash(root, new_import))
.peekable();
if matches.peek().is_some() {
Either::Right(matches)
Either::Right(matches.map(Import::Matched))
} else {
Either::Left(once(import))
Either::Left(once(import.to_string()).map(Import::UnMatched))
}
} else {
Either::Left(once(import))
Either::Left(once(import.to_string()).map(Import::UnMatched))
}
.collect()
}
Expand Down
64 changes: 51 additions & 13 deletions src/rust/engine/dep_inference/src/javascript/mod.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
// Copyright 2023 Pants project contributors (see CONTRIBUTORS.md).
// Licensed under the Apache License, Version 2.0 (see LICENSE).
use std::iter::{once, Once};
use std::path::{Path, PathBuf};

use fnv::FnvHashSet as HashSet;
use fnv::{FnvHashMap as HashMap, FnvHashSet as HashSet};
use itertools::Either;
use serde_derive::{Deserialize, Serialize};
use tree_sitter::{Node, Parser};

use protos::gen::pants::cache::JavascriptInferenceMetadata;
use protos::gen::pants::cache::{
javascript_inference_metadata::ImportPattern, JavascriptInferenceMetadata,
};

use crate::javascript::import_pattern::imports_from_patterns;
use crate::javascript::import_pattern::{imports_from_patterns, Import};
use crate::javascript::util::normalize_path;

mod import_pattern;
Expand All @@ -24,26 +28,60 @@ pub struct ParsedJavascriptDependencies {
pub package_imports: HashSet<String>,
}

fn patterns_as_lookup(patterns: Vec<ImportPattern>) -> HashMap<String, Vec<String>> {
patterns
.into_iter()
.map(|pattern| (pattern.pattern, pattern.replacements))
.collect()
}

fn placed_at_root(package_root: &str, string: &str) -> bool {
!package_root.is_empty() && string.starts_with(package_root)
}

fn is_relative_specifier(string: &str) -> bool {
['.', '/'].into_iter().any(|p| string.starts_with(p))
}

fn match_and_extend_with_config_candidates(
string: String,
paths: &HashMap<String, Vec<String>>,
config_root: Option<&str>,
) -> Either<Once<Import>, impl Iterator<Item = Import>> {
if let Some(imports) = config_root.map(|root| imports_from_patterns(root, paths, &string)) {
Either::Right(imports.into_iter().chain(once(Import::UnMatched(string))))
} else {
Either::Left(once(Import::UnMatched(string)))
}
}

pub fn get_dependencies(
contents: &str,
filepath: PathBuf,
metadata: JavascriptInferenceMetadata,
) -> Result<ParsedJavascriptDependencies, String> {
let patterns = metadata
.import_patterns
.into_iter()
.map(|pattern| (pattern.pattern, pattern.replacements))
.collect();
let import_patterns = patterns_as_lookup(metadata.import_patterns);
let paths = patterns_as_lookup(metadata.paths);

let mut collector = ImportCollector::new(contents);
collector.collect();
let (relative_files, packages): (HashSet<String>, HashSet<String>) = collector
.imports
.into_iter()
.flat_map(|import| imports_from_patterns(&metadata.package_root, &patterns, import))
.flat_map(|import| imports_from_patterns(&metadata.package_root, &import_patterns, &import))
.flat_map(|import| match import {
Import::UnMatched(string) if !is_relative_specifier(&string) => {
match_and_extend_with_config_candidates(
string,
&paths,
metadata.config_root.as_deref(),
)
}
matched => Either::Left(once(matched)),
})
.flatten()
.partition(|import| {
import.starts_with('.')
|| import.starts_with('/')
|| (!metadata.package_root.is_empty() && import.starts_with(&metadata.package_root))
is_relative_specifier(import) || placed_at_root(&metadata.package_root, import)
});
Ok(ParsedJavascriptDependencies {
file_imports: normalize_from_path(&metadata.package_root, filepath, relative_files),
Expand All @@ -63,7 +101,7 @@ fn normalize_from_path(
let path = Path::new(&string);
if path.has_root() {
string
} else if path.starts_with(root) && !root.is_empty() {
} else if placed_at_root(root, &string) {
normalize_path(path).map_or(string, |path| path.to_string_lossy().to_string())
} else {
normalize_path(&directory.join(path))
Expand Down
Loading

0 comments on commit b284ec9

Please sign in to comment.