diff --git a/wake/cli/detect.py b/wake/cli/detect.py index 2673ebd7..cafca94e 100644 --- a/wake/cli/detect.py +++ b/wake/cli/detect.py @@ -652,15 +652,16 @@ def process_detection(detection: Detection) -> Dict[str, Any]: if loaded["system"] == "Windows": wake_contracts_path = PureWindowsPath(loaded["wake_contracts_path"]) + original_project_root = PureWindowsPath(loaded["project_root"]) else: wake_contracts_path = PurePosixPath(loaded["wake_contracts_path"]) + original_project_root = PurePosixPath(loaded["project_root"]) - config = WakeConfig.fromdict(loaded["config"], wake_contracts_path=wake_contracts_path) - original_project_root = Path(loaded["project_root"]) + config = WakeConfig.fromdict(loaded["config"], wake_contracts_path=wake_contracts_path, paths_mode=loaded["system"]) # add project root as an include path (for solc to resolve imports correctly) if config.project_root_path != original_project_root: - config.update({"compiler": {"solc": {"include_paths": set(config.compiler.solc.include_paths) | {original_project_root}, }}}, []) + config.update({"compiler": {"solc": {"include_paths": set(config.compiler.solc.include_paths) | {original_project_root}, }}}, [], paths_mode=loaded["system"]) modified_files = { Path(path): source['content'].encode("utf-8") for path, source in loaded["sources"].items() diff --git a/wake/compiler/build_data_model.py b/wake/compiler/build_data_model.py index eb810b6d..0ad6d55e 100644 --- a/wake/compiler/build_data_model.py +++ b/wake/compiler/build_data_model.py @@ -1,5 +1,5 @@ import weakref -from pathlib import Path +from pathlib import Path, PurePath from types import MappingProxyType from typing import Any, Dict, FrozenSet, List, Optional @@ -104,9 +104,9 @@ class ProjectBuildInfo(BuildInfoModel): compilation_units: Dict[str, CompilationUnitBuildInfo] source_units_info: Dict[str, SourceUnitInfo] - allow_paths: FrozenSet[Path] - exclude_paths: FrozenSet[Path] - include_paths: FrozenSet[Path] + allow_paths: FrozenSet[PurePath] + exclude_paths: FrozenSet[PurePath] + include_paths: FrozenSet[PurePath] settings: SolcInputSettings target_solidity_version: Optional[SolidityVersion] wake_version: str diff --git a/wake/config/data_model.py b/wake/config/data_model.py index cf0d9b4b..094450f9 100644 --- a/wake/config/data_model.py +++ b/wake/config/data_model.py @@ -1,9 +1,9 @@ import re from dataclasses import astuple -from pathlib import Path -from typing import Dict, FrozenSet, List, Optional +from pathlib import Path, PurePath, PurePosixPath, PureWindowsPath +from typing import Dict, FrozenSet, List, Optional, Iterable -from pydantic import BaseModel, ConfigDict, Field, PlainSerializer, field_serializer +from pydantic import BaseModel, ConfigDict, Field, PlainSerializer, field_serializer, field_validator, ValidationInfo from pydantic.dataclasses import dataclass from pydantic.functional_validators import BeforeValidator from typing_extensions import Annotated @@ -20,6 +20,16 @@ class WakeConfigModel(BaseModel): ) +def normalize_paths(paths: Iterable[str], info: ValidationInfo) -> FrozenSet[PurePath]: + if info.context and info.context.get("paths_mode") is not None: + if info.context["paths_mode"] == "Windows": + return frozenset(PureWindowsPath(path) for path in paths) + else: + return frozenset(PurePosixPath(path) for path in paths) + else: + return frozenset(Path(path).resolve() for path in paths) + + @dataclass class SolcRemapping: context: Optional[str] @@ -96,15 +106,11 @@ def convert_remapping(v): class SolcConfig(WakeConfigModel): - allow_paths: FrozenSet[ - Annotated[Path, BeforeValidator(lambda p: Path(p).resolve())] - ] = frozenset() + allow_paths: FrozenSet[PurePath] = frozenset() """Wake should set solc `--allow-paths` automatically. This option allows to specify additional allowed paths.""" evm_version: Optional[EvmVersionEnum] = None """Version of the EVM to compile for. Leave unset to let the solc decide.""" - exclude_paths: FrozenSet[ - Annotated[Path, BeforeValidator(lambda p: Path(p).resolve())] - ] = Field( + exclude_paths: FrozenSet[PurePath] = Field( default_factory=lambda: frozenset( [ Path.cwd() / "node_modules", @@ -119,9 +125,7 @@ class SolcConfig(WakeConfigModel): """ Solidity files in these paths are excluded from compilation unless imported from a non-excluded file. """ - include_paths: FrozenSet[ - Annotated[Path, BeforeValidator(lambda p: Path(p).resolve())] - ] = Field(default_factory=lambda: frozenset([Path.cwd() / "node_modules"])) + include_paths: FrozenSet[PurePath] = Field(default_factory=lambda: frozenset([Path.cwd() / "node_modules"])) """ Paths where to search for Solidity files imported using direct (non-relative) import paths. """ @@ -148,6 +152,8 @@ class SolcConfig(WakeConfigModel): Use new IR-based compiler pipeline. """ + _normalize_paths = field_validator("allow_paths", "include_paths", "exclude_paths", mode="before")(normalize_paths) + @field_serializer("target_version", when_used="json") def serialize_target_version(self, version: Optional[SolidityVersion], info): return str(version) if version is not None else None @@ -207,9 +213,7 @@ class DetectorsConfig(WakeConfigModel): """ Names of detectors that should only be loaded. """ - ignore_paths: FrozenSet[ - Annotated[Path, BeforeValidator(lambda p: Path(p).resolve())] - ] = Field( + ignore_paths: FrozenSet[PurePath] = Field( default_factory=lambda: frozenset( [ Path.cwd() / "venv", @@ -222,9 +226,7 @@ class DetectorsConfig(WakeConfigModel): Detections in these paths must be ignored under all circumstances. Useful for ignoring detections in Solidity test files. """ - exclude_paths: FrozenSet[ - Annotated[Path, BeforeValidator(lambda p: Path(p).resolve())] - ] = Field( + exclude_paths: FrozenSet[PurePath] = Field( default_factory=lambda: frozenset( [ Path.cwd() / "node_modules", @@ -238,6 +240,8 @@ class DetectorsConfig(WakeConfigModel): Useful for ignoring detections in dependencies. """ + _normalize_paths = field_validator("ignore_paths", "exclude_paths", mode="before")(normalize_paths) + # namespace for detector configs class DetectorConfig(WakeConfigModel, extra="allow"): diff --git a/wake/config/wake_config.py b/wake/config/wake_config.py index 1809de58..a09134db 100644 --- a/wake/config/wake_config.py +++ b/wake/config/wake_config.py @@ -228,6 +228,7 @@ def fromdict( *, project_root_path: Optional[Union[str, Path]] = None, wake_contracts_path: Optional[PurePath] = None, + paths_mode: Optional[str] = None, ) -> "WakeConfig": """ Args: @@ -239,7 +240,7 @@ def fromdict( """ instance = cls(project_root_path=project_root_path, wake_contracts_path=wake_contracts_path) with change_cwd(instance.project_root_path): - parsed_config = TopLevelConfig.model_validate(config_dict) + parsed_config = TopLevelConfig.model_validate(config_dict, context={"paths_mode": paths_mode}) instance.__config_raw = parsed_config.model_dump( by_alias=True, exclude_unset=True ) @@ -308,6 +309,7 @@ def update( self, config_dict: Dict[str, Any], deleted_options: Iterable[Tuple[Union[int, str], ...]], + paths_mode: Optional[str] = None, ) -> Dict: """ Update the config with a new dictionary. @@ -320,7 +322,7 @@ def update( Dictionary containing the modified config options. """ with change_cwd(self.project_root_path): - parsed_config = TopLevelConfig.model_validate(config_dict) + parsed_config = TopLevelConfig.model_validate(config_dict, context={"paths_mode": paths_mode}) parsed_config_raw = parsed_config.model_dump(by_alias=True, exclude_unset=True) original_config = deepcopy(self.__config_raw) @@ -346,7 +348,7 @@ def update( except ValueError: pass - self.__config = TopLevelConfig.model_validate(self.__config_raw) + self.__config = TopLevelConfig.model_validate(self.__config_raw, context={"paths_mode": paths_mode}) modified_keys = {} self.__modified_keys( original_config, diff --git a/wake/lsp/features/completion.py b/wake/lsp/features/completion.py index 51637569..58aa4926 100644 --- a/wake/lsp/features/completion.py +++ b/wake/lsp/features/completion.py @@ -574,8 +574,9 @@ async def completion( for include_path in chain( context.config.compiler.solc.include_paths, - [context.config.project_root_path, Path(context.config.wake_contracts_path)], + [context.config.project_root_path, context.config.wake_contracts_path], ): + include_path = Path(include_path) if include_path.is_dir(): for p in include_path.iterdir(): if p.is_dir(): @@ -600,8 +601,9 @@ async def completion( else: for include_path in chain( context.config.compiler.solc.include_paths, - [context.config.project_root_path, Path(context.config.wake_contracts_path)], + [context.config.project_root_path, context.config.wake_contracts_path], ): + include_path = Path(include_path) if include_path.is_dir(): dir = include_path / parent if dir.is_dir(): diff --git a/wake/utils/openzeppelin.py b/wake/utils/openzeppelin.py index 2539a7cd..39a53df2 100644 --- a/wake/utils/openzeppelin.py +++ b/wake/utils/openzeppelin.py @@ -1,6 +1,7 @@ import json import platform import subprocess +from pathlib import Path from typing import Optional from wake.config import WakeConfig @@ -11,7 +12,7 @@ def get_contracts_package_version(config: WakeConfig) -> Optional[SemanticVersio try: node_modules_path = next( path - for path in config.compiler.solc.include_paths + for path in (Path(p) for p in config.compiler.solc.include_paths) if "node_modules" in path.stem and path.is_dir() ) except StopIteration: