diff --git a/.gitignore b/.gitignore index 8dd3e8f5..54f0f8c9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ .vscode -bench_content/**/ +bench_content/**/ \ No newline at end of file diff --git a/src/frontend/configuration.rs b/src/frontend/configuration.rs index 3c23a451..7e6f8511 100644 --- a/src/frontend/configuration.rs +++ b/src/frontend/configuration.rs @@ -9,10 +9,7 @@ use serde::{Deserialize, Serialize}; use crate::{ generator::{DenseLuaGenerator, LuaGenerator, ReadableLuaGenerator, TokenBasedLuaGenerator}, nodes::Block, - rules::{ - bundle::{BundleRequireMode, Bundler}, - get_default_rules, Rule, - }, + rules::{bundle::Bundler, get_default_rules, RequireMode, Rule}, utils::{deserialize_one_or_many, FilterPattern}, DarkluaError, Parser, }; @@ -311,8 +308,8 @@ impl FromStr for GeneratorParameters { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields, rename_all = "snake_case")] pub struct BundleConfiguration { - #[serde(deserialize_with = "crate::utils::string_or_struct")] - require_mode: BundleRequireMode, + #[serde(deserialize_with = "crate::utils::string_or_default")] + require_mode: RequireMode, #[serde(skip_serializing_if = "Option::is_none")] modules_identifier: Option, #[serde(default, skip_serializing_if = "HashSet::is_empty")] @@ -321,7 +318,7 @@ pub struct BundleConfiguration { impl BundleConfiguration { /// Creates a new bundle configuration with the specified require mode. - pub fn new(require_mode: impl Into) -> Self { + pub fn new(require_mode: impl Into) -> Self { Self { require_mode: require_mode.into(), modules_identifier: None, @@ -341,7 +338,7 @@ impl BundleConfiguration { self } - pub(crate) fn require_mode(&self) -> &BundleRequireMode { + pub(crate) fn require_mode(&self) -> &RequireMode { &self.require_mode } @@ -557,7 +554,7 @@ mod test { insta::assert_snapshot!( result.expect_err("deserialization should fail").to_string(), - @"invalid require mode `oops` at line 1 column 26" + @"invalid require mode name `oops` at line 1 column 26" ); } } diff --git a/src/rules/bundle/mod.rs b/src/rules/bundle/mod.rs index 23888b97..f2228438 100644 --- a/src/rules/bundle/mod.rs +++ b/src/rules/bundle/mod.rs @@ -8,8 +8,8 @@ use wax::Program; use crate::nodes::Block; use crate::rules::{ - Context, Rule, RuleConfiguration, RuleConfigurationError, RuleMetadata, RuleProcessResult, - RuleProperties, + Context, RequireMode, Rule, RuleConfiguration, RuleConfigurationError, RuleMetadata, + RuleProcessResult, RuleProperties, }; use crate::Parser; @@ -19,7 +19,7 @@ pub use require_mode::BundleRequireMode; pub const BUNDLER_RULE_NAME: &str = "bundler"; #[derive(Debug)] -pub(crate) struct BundleOptions { +pub struct BundleOptions { parser: Parser, modules_identifier: String, excludes: Option>, @@ -77,14 +77,14 @@ impl BundleOptions { #[derive(Debug)] pub(crate) struct Bundler { metadata: RuleMetadata, - require_mode: BundleRequireMode, + require_mode: RequireMode, options: BundleOptions, } impl Bundler { pub(crate) fn new<'a>( parser: Parser, - require_mode: BundleRequireMode, + require_mode: RequireMode, excludes: impl Iterator, ) -> Self { Self { @@ -136,19 +136,19 @@ const DEFAULT_MODULE_IDENTIFIER: &str = "__DARKLUA_BUNDLE_MODULES"; #[cfg(test)] mod test { use super::*; - use crate::rules::{require::PathRequireMode, Rule}; + use crate::rules::{require::PathRequireMode, RequireMode, Rule}; use insta::assert_json_snapshot; fn new_rule() -> Bundler { Bundler::new( Parser::default(), - BundleRequireMode::default(), + RequireMode::default(), std::iter::empty(), ) } - fn new_rule_with_require_mode(mode: impl Into) -> Bundler { + fn new_rule_with_require_mode(mode: impl Into) -> Bundler { Bundler::new(Parser::default(), mode.into(), std::iter::empty()) } diff --git a/src/rules/bundle/path_require_mode/mod.rs b/src/rules/bundle/path_require_mode/mod.rs index e7abac3f..b1e4f15d 100644 --- a/src/rules/bundle/path_require_mode/mod.rs +++ b/src/rules/bundle/path_require_mode/mod.rs @@ -17,7 +17,7 @@ use crate::nodes::{ use crate::process::{ to_expression, DefaultVisitor, IdentifierTracker, NodeProcessor, NodeVisitor, ScopeVisitor, }; -use crate::rules::require::{is_require_call, match_path_require_call, PathLocator}; +use crate::rules::require::{is_require_call, PathLocator, SingularPathLocator}; use crate::rules::{ Context, ContextBuilder, FlawlessRule, ReplaceReferencedTokens, RuleProcessResult, }; @@ -81,16 +81,20 @@ impl<'a, 'b, 'resources, PathLocatorImpl: PathLocator> } } - fn require_call(&self, call: &FunctionCall) -> Option { + fn require_call( + &self, + call: &FunctionCall, + source: &Path, + ) -> Option<(PathBuf, SingularPathLocator<'_, '_, '_>)> { if is_require_call(call, self) { - match_path_require_call(call) + self.path_locator.match_path_require_call(call, source) } else { None } } fn try_inline_call(&mut self, call: &FunctionCall) -> Option { - let literal_require_path = self.require_call(call)?; + let (literal_require_path, path_locator) = self.require_call(call, &self.source)?; if self.options.is_excluded(&literal_require_path) { log::info!( @@ -101,9 +105,7 @@ impl<'a, 'b, 'resources, PathLocatorImpl: PathLocator> return None; } - let require_path = match self - .path_locator - .find_require_path(&literal_require_path, &self.source) + let require_path = match path_locator.find_require_path(&literal_require_path, &self.source) { Ok(path) => path, Err(err) => { diff --git a/src/rules/bundle/require_mode.rs b/src/rules/bundle/require_mode.rs index 81f13a4c..f67fcee8 100644 --- a/src/rules/bundle/require_mode.rs +++ b/src/rules/bundle/require_mode.rs @@ -1,48 +1,91 @@ -use std::str::FromStr; - -use serde::{Deserialize, Serialize}; - use crate::rules::{ - require::{LuauPathLocator, LuauRequireMode, PathRequireMode, RequirePathLocator}, - RuleProcessResult, + require::{ + HybridPathLocator, LuauPathLocator, LuauRequireMode, PathRequireMode, RequirePathLocator, + RobloxPathLocator, + }, + RequireMode, RequireModeLike, RobloxRequireMode, RuleProcessResult, SingularRequireMode, }; use crate::{nodes::Block, rules::Context}; use super::{path_require_mode, BundleOptions}; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(deny_unknown_fields, rename_all = "snake_case", tag = "name")] -pub enum BundleRequireMode { - Path(PathRequireMode), - Luau(LuauRequireMode), +pub trait BundleRequireMode { + fn process_block( + &self, + block: &mut Block, + context: &Context, + options: &BundleOptions, + ) -> RuleProcessResult; } -impl From for BundleRequireMode { - fn from(mode: PathRequireMode) -> Self { - Self::Path(mode) +impl BundleRequireMode for PathRequireMode { + fn process_block( + &self, + block: &mut Block, + context: &Context, + options: &BundleOptions, + ) -> RuleProcessResult { + let mut require_mode = self.clone(); + require_mode + .initialize(context) + .map_err(|err| err.to_string())?; + + let locator = RequirePathLocator::new( + &require_mode, + context.project_location(), + context.resources(), + ); + + path_require_mode::process_block(block, context, options, locator) } } -impl FromStr for BundleRequireMode { - type Err = String; +impl BundleRequireMode for LuauRequireMode { + fn process_block( + &self, + block: &mut Block, + context: &Context, + options: &BundleOptions, + ) -> RuleProcessResult { + let mut require_mode = self.clone(); + require_mode + .initialize(context) + .map_err(|err| err.to_string())?; + + let locator = LuauPathLocator::new( + &require_mode, + context.project_location(), + context.resources(), + ); - fn from_str(s: &str) -> Result { - Ok(match s { - "path" => Self::Path(Default::default()), - "luau" => Self::Luau(Default::default()), - _ => return Err(format!("invalid require mode `{}`", s)), - }) + path_require_mode::process_block(block, context, options, locator) } } -impl Default for BundleRequireMode { - fn default() -> Self { - Self::Path(Default::default()) +impl BundleRequireMode for RobloxRequireMode { + fn process_block( + &self, + block: &mut Block, + context: &Context, + options: &BundleOptions, + ) -> RuleProcessResult { + let mut require_mode = self.clone(); + require_mode + .initialize(context) + .map_err(|err| err.to_string())?; + + let locator = RobloxPathLocator::new( + &require_mode, + context.project_location(), + context.resources(), + ); + + path_require_mode::process_block(block, context, options, locator) } } -impl BundleRequireMode { - pub(crate) fn process_block( +impl BundleRequireMode for SingularRequireMode { + fn process_block( &self, block: &mut Block, context: &Context, @@ -50,30 +93,37 @@ impl BundleRequireMode { ) -> RuleProcessResult { match self { Self::Path(path_require_mode) => { - let mut require_mode = path_require_mode.clone(); - require_mode - .initialize(context) - .map_err(|err| err.to_string())?; - - let locator = RequirePathLocator::new( - &require_mode, - context.project_location(), - context.resources(), - ); - - path_require_mode::process_block(block, context, options, locator) + path_require_mode.process_block(block, context, options) } Self::Luau(luau_require_mode) => { - let mut require_mode = luau_require_mode.clone(); - require_mode - .initialize(context) - .map_err(|err| err.to_string())?; + luau_require_mode.process_block(block, context, options) + } + Self::Roblox(roblox_require_mode) => { + roblox_require_mode.process_block(block, context, options) + } + } + } +} + +impl BundleRequireMode for RequireMode { + fn process_block( + &self, + block: &mut Block, + context: &Context, + options: &BundleOptions, + ) -> RuleProcessResult { + match self { + RequireMode::Single(singular_require_mode) => { + singular_require_mode.process_block(block, context, options) + } + RequireMode::Hybrid(singular_require_modes) => { + let mut modes = singular_require_modes.clone(); + for mode in modes.iter_mut() { + mode.initialize(context).map_err(|err| err.to_string())?; + } - let locator = LuauPathLocator::new( - &require_mode, - context.project_location(), - context.resources(), - ); + let locator = + HybridPathLocator::new(&modes, context.project_location(), context.resources()); path_require_mode::process_block(block, context, options, locator) } diff --git a/src/rules/convert_require/mod.rs b/src/rules/convert_require/mod.rs index e5f5991a..a2c9f411 100644 --- a/src/rules/convert_require/mod.rs +++ b/src/rules/convert_require/mod.rs @@ -12,23 +12,43 @@ use crate::rules::require::is_require_call; use crate::rules::{ Context, RuleConfiguration, RuleConfigurationError, RuleMetadata, RuleProperties, }; +use crate::DarkluaError; use instance_path::InstancePath; pub use roblox_index_style::RobloxIndexStyle; -pub use roblox_require_mode::RobloxRequireMode; +pub use roblox_require_mode::{parse_roblox, RobloxRequireMode}; use super::{verify_required_properties, PathRequireMode, Rule, RuleProcessResult}; use crate::rules::require::LuauRequireMode; -use std::ffi::OsStr; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; use std::str::FromStr; +/// A representation of how require calls are handled and transformed. +pub trait RequireModeLike { + /// Parses the function to call to check for a `require` call. + /// + /// Returns the singular require mode used within the `require` call. + fn find_require( + &self, + call: &FunctionCall, + context: &Context, + ) -> DarkluaResult>; + fn generate_require( + &self, + path: &Path, + current_mode: &T, + context: &Context, + ) -> DarkluaResult>; + fn is_module_folder_name(&self, path: &Path) -> DarkluaResult; + fn initialize(&mut self, context: &Context) -> DarkluaResult<()>; +} + /// A representation of how require calls are handled and transformed. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields, rename_all = "snake_case", tag = "name")] -pub enum RequireMode { +pub enum SingularRequireMode { /// Handles requires using file system paths Path(PathRequireMode), /// Handles requires using Luau module paths @@ -37,54 +57,78 @@ pub enum RequireMode { Roblox(RobloxRequireMode), } -impl RequireMode { - pub(crate) fn find_require( +impl Default for SingularRequireMode { + fn default() -> Self { + Self::Path(Default::default()) + } +} + +impl From for SingularRequireMode { + fn from(value: PathRequireMode) -> Self { + Self::Path(value) + } +} +impl From for SingularRequireMode { + fn from(value: LuauRequireMode) -> Self { + Self::Luau(value) + } +} +impl From for SingularRequireMode { + fn from(value: RobloxRequireMode) -> Self { + Self::Roblox(value) + } +} + +impl RequireModeLike for SingularRequireMode { + fn find_require( &self, call: &FunctionCall, context: &Context, - ) -> DarkluaResult> { + ) -> DarkluaResult> { match self { - RequireMode::Path(path_mode) => path_mode.find_require(call, context), - RequireMode::Luau(luau_mode) => luau_mode.find_require(call, context), - RequireMode::Roblox(roblox_mode) => roblox_mode.find_require(call, context), + SingularRequireMode::Path(path_mode) => path_mode.find_require(call, context), + SingularRequireMode::Luau(luau_mode) => luau_mode.find_require(call, context), + SingularRequireMode::Roblox(roblox_mode) => roblox_mode.find_require(call, context), } } - fn generate_require( + fn generate_require( &self, path: &Path, - current_mode: &Self, + current_mode: &T, context: &Context, ) -> DarkluaResult> { match self { - RequireMode::Path(path_mode) => path_mode.generate_require(path, current_mode, context), - RequireMode::Luau(luau_mode) => luau_mode.generate_require(path, current_mode, context), - RequireMode::Roblox(roblox_mode) => { + SingularRequireMode::Path(path_mode) => { + path_mode.generate_require(path, current_mode, context) + } + SingularRequireMode::Luau(luau_mode) => { + luau_mode.generate_require(path, current_mode, context) + } + SingularRequireMode::Roblox(roblox_mode) => { roblox_mode.generate_require(path, current_mode, context) } } } - fn is_module_folder_name(&self, path: &Path) -> bool { + fn is_module_folder_name(&self, path: &Path) -> DarkluaResult { match self { - RequireMode::Path(path_mode) => path_mode.is_module_folder_name(path), - RequireMode::Luau(luau_mode) => luau_mode.is_module_folder_name(path), - RequireMode::Roblox(_roblox_mode) => { - matches!(path.file_stem().and_then(OsStr::to_str), Some("init")) - } + SingularRequireMode::Path(path_mode) => path_mode.is_module_folder_name(path), + SingularRequireMode::Luau(luau_mode) => luau_mode.is_module_folder_name(path), + SingularRequireMode::Roblox(roblox_mode) => roblox_mode.is_module_folder_name(path), } } fn initialize(&mut self, context: &Context) -> DarkluaResult<()> { match self { - RequireMode::Roblox(roblox_mode) => roblox_mode.initialize(context), - RequireMode::Path(path_mode) => path_mode.initialize(context), - RequireMode::Luau(luau_mode) => luau_mode.initialize(context), + SingularRequireMode::Roblox(roblox_mode) => roblox_mode.initialize(context), + SingularRequireMode::Path(path_mode) => path_mode.initialize(context), + SingularRequireMode::Luau(luau_mode) => luau_mode.initialize(context), } } } -impl FromStr for RequireMode { +impl FromStr for SingularRequireMode { type Err = String; fn from_str(s: &str) -> Result { @@ -97,11 +141,134 @@ impl FromStr for RequireMode { } } +/// A representation of how require calls are handled and transformed. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(untagged)] +pub enum RequireMode { + Single(SingularRequireMode), + Hybrid(Vec), +} + +impl Default for RequireMode { + fn default() -> Self { + Self::Single(Default::default()) + } +} + +impl FromStr for RequireMode { + type Err = String; + + fn from_str(s: &str) -> Result { + Ok(match s { + "path" => Self::Single(SingularRequireMode::Path(Default::default())), + "luau" => Self::Single(SingularRequireMode::Luau(Default::default())), + "roblox" => Self::Single(SingularRequireMode::Roblox(Default::default())), + "hybrid" => Self::Hybrid(vec![ + SingularRequireMode::Path(Default::default()), + SingularRequireMode::Luau(Default::default()), + SingularRequireMode::Roblox(Default::default()), + ]), + _ => return Err(format!("invalid require mode name `{}`", s)), + }) + } +} + +impl From for RequireMode { + fn from(value: PathRequireMode) -> Self { + Self::Single(value.into()) + } +} +impl From for RequireMode { + fn from(value: LuauRequireMode) -> Self { + Self::Single(value.into()) + } +} +impl From for RequireMode { + fn from(value: RobloxRequireMode) -> Self { + Self::Single(value.into()) + } +} +impl> From> for RequireMode { + fn from(value: Vec) -> Self { + Self::Hybrid(value.into_iter().map(Into::into).collect()) + } +} + +impl RequireModeLike for RequireMode { + fn find_require( + &self, + call: &FunctionCall, + context: &Context, + ) -> DarkluaResult> { + match self { + RequireMode::Single(singular_require_mode) => { + singular_require_mode.find_require(call, context) + } + RequireMode::Hybrid(singular_require_modes) => { + for mode in singular_require_modes { + match mode.find_require(call, context) { + Ok(Some(x)) => return Ok(Some(x)), + x => println!("nooo: {x:?}"), + } + } + + Err(DarkluaError::custom("unable to find valid require"))? + } + } + } + + fn generate_require( + &self, + path: &Path, + current_mode: &T, + context: &Context, + ) -> DarkluaResult> { + match self { + RequireMode::Single(singular_require_mode) => { + singular_require_mode.generate_require(path, current_mode, context) + } + RequireMode::Hybrid(singular_require_modes) => { + for mode in singular_require_modes { + if let Ok(Some(x)) = mode.generate_require(path, current_mode, context) { + return Ok(Some(x)); + } + } + + Err(DarkluaError::custom("unable to find valid require"))? + } + } + } + + fn is_module_folder_name(&self, path: &Path) -> DarkluaResult { + match self { + RequireMode::Single(singular_require_mode) => { + singular_require_mode.is_module_folder_name(path) + } + RequireMode::Hybrid(_singular_require_modes) => Err(DarkluaError::custom( + "cannot get module folder name of hybrid", + )), + } + } + + fn initialize(&mut self, context: &Context) -> DarkluaResult<()> { + match self { + RequireMode::Single(singular_require_mode) => singular_require_mode.initialize(context), + RequireMode::Hybrid(singular_require_modes) => { + for mode in singular_require_modes { + mode.initialize(context)?; + } + + Ok(()) + } + } + } +} + #[derive(Debug, Clone)] struct RequireConverter<'a> { identifier_tracker: IdentifierTracker, current: RequireMode, - target: RequireMode, + target: SingularRequireMode, context: &'a Context<'a, 'a, 'a>, } @@ -120,7 +287,7 @@ impl DerefMut for RequireConverter<'_> { } impl<'a> RequireConverter<'a> { - fn new(current: RequireMode, target: RequireMode, context: &'a Context) -> Self { + fn new(current: RequireMode, target: SingularRequireMode, context: &'a Context) -> Self { Self { identifier_tracker: IdentifierTracker::new(), current, @@ -130,12 +297,12 @@ impl<'a> RequireConverter<'a> { } fn try_require_conversion(&mut self, call: &mut FunctionCall) -> DarkluaResult<()> { - if let Some(require_path) = self.current.find_require(call, self.context)? { + if let Some((require_path, require_mode)) = self.current.find_require(call, self.context)? { log::trace!("found require path `{}`", require_path.display()); if let Some(new_arguments) = self.target - .generate_require(&require_path, &self.current, self.context)? + .generate_require(&require_path, &require_mode, self.context)? { call.set_arguments(new_arguments); } @@ -164,15 +331,15 @@ pub const CONVERT_REQUIRE_RULE_NAME: &str = "convert_require"; pub struct ConvertRequire { metadata: RuleMetadata, current: RequireMode, - target: RequireMode, + target: SingularRequireMode, } impl Default for ConvertRequire { fn default() -> Self { Self { metadata: RuleMetadata::default(), - current: RequireMode::Path(Default::default()), - target: RequireMode::Roblox(Default::default()), + current: RequireMode::Single(SingularRequireMode::Path(Default::default())), + target: SingularRequireMode::Roblox(Default::default()), } } } @@ -205,7 +372,13 @@ impl RuleConfiguration for ConvertRequire { self.current = value.expect_require_mode(&key)?; } "target" => { - self.target = value.expect_require_mode(&key)?; + self.target = match value.expect_require_mode(&key)? { + RequireMode::Single(singular_require_mode) => singular_require_mode, + RequireMode::Hybrid(_) => Err(RuleConfigurationError::UnexpectedValue { + property: String::from("target"), + message: String::from("target can only be a singular require mode"), + })?, + }; } _ => return Err(RuleConfigurationError::UnexpectedProperty(key)), } diff --git a/src/rules/convert_require/roblox_require_mode.rs b/src/rules/convert_require/roblox_require_mode.rs index d38ffbf2..9bb4deaf 100644 --- a/src/rules/convert_require/roblox_require_mode.rs +++ b/src/rules/convert_require/roblox_require_mode.rs @@ -2,11 +2,11 @@ use serde::{Deserialize, Serialize}; use crate::{ frontend::DarkluaResult, - nodes::{Arguments, FunctionCall, Prefix}, + nodes::{Arguments, Expression, FieldExpression, FunctionCall, IndexExpression, Prefix}, rules::{ convert_require::rojo_sourcemap::RojoSourcemap, require::path_utils::{get_relative_parent_path, get_relative_path}, - Context, + Context, RequireModeLike, SingularRequireMode, }, utils, DarkluaError, }; @@ -15,7 +15,7 @@ use std::path::{Component, Path, PathBuf}; use super::{ instance_path::{get_parent_instance, script_identifier}, - RequireMode, RobloxIndexStyle, + RobloxIndexStyle, }; #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] @@ -28,8 +28,8 @@ pub struct RobloxRequireMode { cached_sourcemap: Option, } -impl RobloxRequireMode { - pub(crate) fn initialize(&mut self, context: &Context) -> DarkluaResult<()> { +impl RequireModeLike for RobloxRequireMode { + fn initialize(&mut self, context: &Context) -> DarkluaResult<()> { if let Some(ref rojo_sourcemap_path) = self .rojo_sourcemap .as_ref() @@ -58,19 +58,26 @@ impl RobloxRequireMode { Ok(()) } - pub(crate) fn find_require( + fn is_module_folder_name(&self, path: &Path) -> DarkluaResult { + Ok(matches!( + path.file_stem().and_then(std::ffi::OsStr::to_str), + Some("init") + )) + } + + fn find_require( &self, - _call: &FunctionCall, - _context: &Context, - ) -> DarkluaResult> { - Err(DarkluaError::custom("unsupported initial require mode") - .context("Roblox require mode cannot be used as the current require mode")) + call: &FunctionCall, + context: &Context, + ) -> DarkluaResult> { + parse_roblox(call, context.current_path()) + .map(|x| x.map(|y| (y, SingularRequireMode::Roblox(self.clone())))) } - pub(crate) fn generate_require( + fn generate_require( &self, require_path: &Path, - current: &RequireMode, + current: &T, context: &Context, ) -> DarkluaResult> { let source_path = utils::normalize_path(context.current_path()); @@ -146,7 +153,7 @@ impl RobloxRequireMode { ); let require_is_module_folder_name = - current.is_module_folder_name(&relative_require_path); + current.is_module_folder_name(&relative_require_path)?; // if we are about to make a require to a path like `./x/y/z/init.lua` // we can pop the last component from the path let take_components = relative_require_path @@ -156,7 +163,7 @@ impl RobloxRequireMode { let mut path_components = relative_require_path.components().take(take_components); if let Some(first_component) = path_components.next() { - let source_is_module_folder_name = current.is_module_folder_name(&source_path); + let source_is_module_folder_name = current.is_module_folder_name(&source_path)?; let instance_path = path_components.try_fold( match first_component { @@ -232,3 +239,215 @@ impl RobloxRequireMode { } } } + +#[derive(Deserialize)] +struct RojoTree { + #[serde(rename = "$path")] + path: PathBuf, +} + +#[derive(Deserialize)] +struct RojoProject { + tree: RojoTree, +} + +pub fn parse_roblox(call: &FunctionCall, starting_path: &Path) -> DarkluaResult> { + let Arguments::Tuple(args) = call.get_arguments() else { + Err( + DarkluaError::custom("unexpected require call, only accepts tuples") + .context("while finding roblox requires"), + )? + }; + + let mut path_builder = Vec::::new(); + let mut current_path = starting_path.to_path_buf(); + let mut parented = false; + + match args.iter_values().next() { + Some(Expression::Field(field)) => { + parse_roblox_field(field, &mut path_builder, &mut current_path, &mut parented)? + } + Some(Expression::Index(index)) => { + parse_roblox_index(index, &mut path_builder, &mut current_path, &mut parented)? + } + Some(Expression::Call(call)) => { + parse_ffc_wfc(call, &mut path_builder, &mut current_path, &mut parented)? + } + _ => Err(DarkluaError::custom( + "unexpected require argument, only accepts fields or indexes", + ) + .context("while getting roblox path"))?, + }; + + if let Some(back) = path_builder.last() { + if back != "script" { + Err( + DarkluaError::custom("roblox requires must start with `script.`") + .context("while getting roblox require path"), + )? + } else { + path_builder.pop(); + } + } + + while let Some(x) = path_builder.pop() { + current_path.push(x) + } + + let mut base_path = starting_path.to_path_buf(); + base_path.pop(); + + let mut project_json = current_path.clone(); + project_json.push("default.project.json"); + if let Ok(project_json) = std::fs::File::open(project_json) { + let project_data: RojoProject = serde_json::from_reader(project_json)?; + current_path.push(project_data.tree.path) + } + + Ok(Some(current_path)) +} + +fn parse_ffc_wfc( + call: &FunctionCall, + path_builder: &mut Vec, + current_path: &mut PathBuf, + parented: &mut bool, +) -> DarkluaResult<()> { + if let Some(x) = call.get_method() { + if !matches!(x.get_name().as_str(), "FindFirstChild" | "WaitForChild") { + Err(DarkluaError::custom("invalid method call found") + .context("while parsing FFC/WFC in require"))? + } + } else { + Err(DarkluaError::custom("only method calls are accepted") + .context("while parsing FFC/WFC in require"))? + } + + match call.get_arguments() { + Arguments::String(x) => path_builder.push(x.clone().into_string().unwrap_or_default()), + Arguments::Tuple(x) => path_builder.push( + x.iter_values() + .next() + .and_then(|x| match x { + Expression::String(x) => x.clone().into_string(), + _ => None, + }) + .ok_or( + DarkluaError::custom("no arguments found for method call") + .context("while parsing FFC/WFC in require"), + )?, + ), + _ => Err(DarkluaError::custom( + "only string and tuple arguments are accepted for method calls", + ) + .context("while parsing FFC/WFC in require"))?, + }; + + parse_roblox_prefix(call.get_prefix(), path_builder, current_path, parented)?; + + Ok(()) +} + +fn parse_roblox_prefix( + prefix: &Prefix, + path_builder: &mut Vec, + current_path: &mut PathBuf, + parented: &mut bool, +) -> DarkluaResult<()> { + match prefix { + Prefix::Field(x) => parse_roblox_field(x, path_builder, current_path, parented)?, + Prefix::Index(x) => parse_roblox_index(x, path_builder, current_path, parented)?, + Prefix::Identifier(x) => { + handle_roblox_script_parent(x.get_name(), path_builder, current_path, parented)? + } + Prefix::Call(x) => parse_ffc_wfc(x, path_builder, current_path, parented)?, + _ => Err( + DarkluaError::custom("unexpected prefix, only constants accepted") + .context("while parsing roblox require"), + )?, + }; + Ok(()) +} + +fn parse_roblox_expression( + expression: &Expression, + path_builder: &mut Vec, + current_path: &mut PathBuf, + parented: &mut bool, +) -> DarkluaResult<()> { + match expression { + Expression::Field(x) => parse_roblox_field(x, path_builder, current_path, parented)?, + Expression::Index(x) => parse_roblox_index(x, path_builder, current_path, parented)?, + Expression::Identifier(x) => { + handle_roblox_script_parent(x.get_name(), path_builder, current_path, parented)? + } + Expression::String(x) => handle_roblox_script_parent( + x.get_string_value().unwrap_or_default(), + path_builder, + current_path, + parented, + )?, + Expression::Call(x) => parse_ffc_wfc(x, path_builder, current_path, parented)?, + _ => Err( + DarkluaError::custom("unexpected expression, only constants accepted") + .context("while parsing roblox require"), + )?, + }; + Ok(()) +} + +fn parse_roblox_field( + field: &FieldExpression, + path_builder: &mut Vec, + current_path: &mut PathBuf, + parented: &mut bool, +) -> DarkluaResult<()> { + handle_roblox_script_parent( + field.get_field().get_name(), + path_builder, + current_path, + parented, + )?; + parse_roblox_prefix(field.get_prefix(), path_builder, current_path, parented) +} + +fn parse_roblox_index( + index: &IndexExpression, + path_builder: &mut Vec, + current_path: &mut PathBuf, + parented: &mut bool, +) -> DarkluaResult<()> { + parse_roblox_expression(index.get_index(), path_builder, current_path, parented)?; + parse_roblox_prefix(index.get_prefix(), path_builder, current_path, parented) +} + +fn handle_roblox_script_parent( + str: &str, + path_builder: &mut Vec, + current_path: &mut PathBuf, + parented: &mut bool, +) -> DarkluaResult<()> { + match str { + "script" => { + while let Some(back) = path_builder.last() { + if !(*parented) { + current_path.pop(); + *parented = true; + } + + if back == "Parent" { + path_builder.pop(); + } else { + break; + } + } + } + "Parent" => { + *parented = true; + current_path.pop(); + } + _ => {} + }; + path_builder.push(str.to_string()); + Ok(()) +} diff --git a/src/rules/require/hybrid_path_locator.rs b/src/rules/require/hybrid_path_locator.rs new file mode 100644 index 00000000..6671d5df --- /dev/null +++ b/src/rules/require/hybrid_path_locator.rs @@ -0,0 +1,134 @@ +use std::path::{Path, PathBuf}; + +use crate::{ + nodes::FunctionCall, + rules::{ + require::{LuauPathLocator, PathLocator, RequirePathLocator, RobloxPathLocator}, + SingularRequireMode, + }, + Resources, +}; + +#[derive(Debug)] +pub enum SingularPathLocator<'a, 'b, 'c> { + Path(RequirePathLocator<'a, 'b, 'c>), + Luau(LuauPathLocator<'a, 'b, 'c>), + Roblox(RobloxPathLocator<'a, 'b, 'c>), +} + +impl<'a, 'b, 'c> SingularPathLocator<'a, 'b, 'c> { + fn from( + value: &'a SingularRequireMode, + extra_module_relative_location: &'b Path, + resources: &'c Resources, + ) -> Self { + match value { + SingularRequireMode::Path(path_require_mode) => Self::Path(RequirePathLocator::new( + path_require_mode, + extra_module_relative_location, + resources, + )), + SingularRequireMode::Luau(luau_require_mode) => Self::Luau(LuauPathLocator::new( + luau_require_mode, + extra_module_relative_location, + resources, + )), + SingularRequireMode::Roblox(roblox_require_mode) => { + Self::Roblox(RobloxPathLocator::new( + roblox_require_mode, + extra_module_relative_location, + resources, + )) + } + } + } +} + +impl PathLocator for SingularPathLocator<'_, '_, '_> { + fn match_path_require_call( + &self, + call: &FunctionCall, + source: &Path, + ) -> Option<(PathBuf, SingularPathLocator<'_, '_, '_>)> { + match self { + SingularPathLocator::Path(require_path_locator) => { + require_path_locator.match_path_require_call(call, source) + } + SingularPathLocator::Luau(luau_path_locator) => { + luau_path_locator.match_path_require_call(call, source) + } + SingularPathLocator::Roblox(roblox_path_locator) => { + roblox_path_locator.match_path_require_call(call, source) + } + } + } + + fn find_require_path( + &self, + path: impl Into, + source: &Path, + ) -> Result { + match self { + SingularPathLocator::Path(require_path_locator) => { + require_path_locator.find_require_path(path, source) + } + SingularPathLocator::Luau(luau_path_locator) => { + luau_path_locator.find_require_path(path, source) + } + SingularPathLocator::Roblox(roblox_path_locator) => { + roblox_path_locator.find_require_path(path, source) + } + } + } +} + +#[derive(Debug)] +pub(crate) struct HybridPathLocator<'a, 'b, 'resources> { + path_locators: Vec>, +} + +impl<'a, 'b, 'c> HybridPathLocator<'a, 'b, 'c> { + pub(crate) fn new( + require_modes: &'a Vec, + extra_module_relative_location: &'b Path, + resources: &'c Resources, + ) -> Self { + let mut path_locators = Vec::new(); + + for mode in require_modes { + path_locators.push(SingularPathLocator::from( + mode, + extra_module_relative_location, + resources, + )) + } + + Self { path_locators } + } +} + +impl PathLocator for HybridPathLocator<'_, '_, '_> { + fn match_path_require_call( + &self, + call: &FunctionCall, + source: &Path, + ) -> Option<(PathBuf, SingularPathLocator<'_, '_, '_>)> { + for locator in &self.path_locators { + if let Some(x) = locator.match_path_require_call(call, source) { + return Some(x); + } + } + + None + } + + fn find_require_path( + &self, + _path: impl Into, + _source: &Path, + ) -> Result { + Err(crate::DarkluaError::custom( + "this cannot be called within this context", + )) + } +} diff --git a/src/rules/require/luau_path_locator.rs b/src/rules/require/luau_path_locator.rs index 3528711a..02cfc274 100644 --- a/src/rules/require/luau_path_locator.rs +++ b/src/rules/require/luau_path_locator.rs @@ -1,12 +1,15 @@ use std::path::{Path, PathBuf}; use super::{path_iterator, LuauRequireMode}; +use crate::rules::require::hybrid_path_locator::SingularPathLocator; +use crate::rules::require::match_path_require_call; use crate::rules::require::path_utils::{get_relative_parent_path, is_require_relative}; +use crate::rules::RequireModeLike; use crate::{utils, DarkluaError, Resources}; /// A path locator specifically for Luau require mode that implements /// the behavior defined in the Luau RFCs for module path resolution. -#[derive(Debug)] +#[derive(Clone, Debug)] pub(crate) struct LuauPathLocator<'a, 'b, 'resources> { luau_require_mode: &'a LuauRequireMode, extra_module_relative_location: &'b Path, @@ -28,6 +31,14 @@ impl<'a, 'b, 'c> LuauPathLocator<'a, 'b, 'c> { } impl super::PathLocator for LuauPathLocator<'_, '_, '_> { + fn match_path_require_call( + &self, + call: &crate::nodes::FunctionCall, + _source: &Path, + ) -> Option<(PathBuf, SingularPathLocator<'_, '_, '_>)> { + match_path_require_call(call).map(|x| (x, SingularPathLocator::Luau(self.clone()))) + } + fn find_require_path( &self, path: impl Into, @@ -41,7 +52,7 @@ impl super::PathLocator for LuauPathLocator<'_, '_, '_> { ); if is_require_relative(&path) { - if self.luau_require_mode.is_module_folder_name(source) { + if self.luau_require_mode.is_module_folder_name(source)? { path = get_relative_parent_path(get_relative_parent_path(source)).join(path); } else { path = get_relative_parent_path(source).join(path); diff --git a/src/rules/require/luau_require_mode.rs b/src/rules/require/luau_require_mode.rs index 58400536..1de212c0 100644 --- a/src/rules/require/luau_require_mode.rs +++ b/src/rules/require/luau_require_mode.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::frontend::DarkluaResult; use crate::nodes::{Arguments, FunctionCall, StringExpression}; use crate::rules::require::{match_path_require_call, path_utils, LuauPathLocator, PathLocator}; -use crate::rules::{Context, RequireMode}; +use crate::rules::{Context, RequireModeLike, SingularRequireMode}; use crate::utils; use crate::DarkluaError; @@ -38,20 +38,8 @@ impl Default for LuauRequireMode { } } -impl LuauRequireMode { - /// Set if the require mode should use `.luaurc` configuration to resolve aliases. - pub fn with_configuration(mut self, use_luau_configuration: bool) -> Self { - self.use_luau_configuration = use_luau_configuration; - self - } - - /// Add a new Luau alias to the require mode. - pub fn with_alias(mut self, name: impl Into, path: impl Into) -> Self { - self.aliases.insert(name.into(), path.into()); - self - } - - pub(crate) fn initialize(&mut self, context: &Context) -> Result<(), DarkluaError> { +impl RequireModeLike for LuauRequireMode { + fn initialize(&mut self, context: &Context) -> Result<(), DarkluaError> { if !self.use_luau_configuration { self.luau_rc_aliases.take(); return Ok(()); @@ -69,22 +57,17 @@ impl LuauRequireMode { Ok(()) } - #[inline] - pub(crate) fn module_folder_name(&self) -> &str { - "init" - } - - pub(crate) fn is_module_folder_name(&self, path: &Path) -> bool { + fn is_module_folder_name(&self, path: &Path) -> DarkluaResult { let expect_value = Some(self.module_folder_name()); - path.file_name().and_then(OsStr::to_str) == expect_value - || path.file_stem().and_then(OsStr::to_str) == expect_value + Ok(path.file_name().and_then(OsStr::to_str) == expect_value + || path.file_stem().and_then(OsStr::to_str) == expect_value) } - pub(crate) fn find_require( + fn find_require( &self, call: &FunctionCall, context: &Context, - ) -> DarkluaResult> { + ) -> DarkluaResult> { if let Some(literal_path) = match_path_require_call(call) { let path_locator = LuauPathLocator::new(self, context.project_location(), context.resources()); @@ -92,16 +75,19 @@ impl LuauRequireMode { let required_path = path_locator.find_require_path(literal_path, context.current_path())?; - Ok(Some(required_path)) + Ok(Some(( + required_path, + SingularRequireMode::Luau(self.clone()), + ))) } else { Ok(None) } } - pub(crate) fn generate_require( + fn generate_require( &self, require_path: &Path, - _current: &RequireMode, + _current: &T, context: &Context<'_, '_, '_>, ) -> Result, crate::DarkluaError> { let source_path = utils::normalize_path(context.current_path()); @@ -119,8 +105,8 @@ impl LuauRequireMode { ); // if the source path is 'init.luau' or 'init.lua', we need to use @self - if self.is_module_folder_name(&source_path) { - let require_is_module_folder_name = self.is_module_folder_name(require_path); + if self.is_module_folder_name(&source_path)? { + let require_is_module_folder_name = self.is_module_folder_name(require_path)?; // if we are about to make a require to a path like `./x/y/z/init.lua` // we can pop the last component from the path let take_components = require_path @@ -190,7 +176,7 @@ impl LuauRequireMode { relative_require_path.display() ); - if self.is_module_folder_name(&source_path) { + if self.is_module_folder_name(&source_path)? { if relative_require_path.starts_with(".") { let mut new_path = PathBuf::from("@self"); new_path.extend(relative_require_path.components().skip(1)); @@ -216,7 +202,7 @@ impl LuauRequireMode { } }; - if self.is_module_folder_name(&generated_path) { + if self.is_module_folder_name(&generated_path)? { generated_path.pop(); } else if matches!(generated_path.extension(), Some(extension) if extension == "lua" || extension == "luau") { @@ -225,6 +211,25 @@ impl LuauRequireMode { path_utils::write_require_path(&generated_path).map(generate_require_arguments) } +} + +impl LuauRequireMode { + /// Set if the require mode should use `.luaurc` configuration to resolve aliases. + pub fn with_configuration(mut self, use_luau_configuration: bool) -> Self { + self.use_luau_configuration = use_luau_configuration; + self + } + + /// Add a new Luau alias to the require mode. + pub fn with_alias(mut self, name: impl Into, path: impl Into) -> Self { + self.aliases.insert(name.into(), path.into()); + self + } + + #[inline] + pub(crate) fn module_folder_name(&self) -> &str { + "init" + } pub(crate) fn get_source(&self, name: &str, rel: &Path) -> Option { log::trace!( @@ -286,7 +291,10 @@ mod test { let context = make_context_with_files("/project/src/main.luau", &["/project/src/./module"]); let call = make_call("./module"); let result = mode.find_require(&call, &context).unwrap(); - assert_eq!(result, Some(PathBuf::from("/project/src/./module"))); + assert_eq!( + result.map(|(x, _)| x), + Some(PathBuf::from("/project/src/./module")) + ); } #[test] @@ -301,7 +309,7 @@ mod test { .components() .collect::(); let norm = |p: &std::path::PathBuf| p.components().collect::(); - assert_eq!(result.map(|p| norm(&p)), Some(norm(&expected))); + assert_eq!(result.map(|(p, _)| norm(&p)), Some(norm(&expected))); } mod default { @@ -323,35 +331,45 @@ mod test { fn is_false_for_regular_name() { let require_mode = LuauRequireMode::default(); - assert!(!require_mode.is_module_folder_name(Path::new("oops.lua"))); + assert!(!require_mode + .is_module_folder_name(Path::new("oops.lua")) + .unwrap()); } #[test] fn is_true_for_init_lua() { let require_mode = LuauRequireMode::default(); - assert!(require_mode.is_module_folder_name(Path::new("init.lua"))); + assert!(require_mode + .is_module_folder_name(Path::new("init.lua")) + .unwrap()); } #[test] fn is_true_for_init_luau() { let require_mode = LuauRequireMode::default(); - assert!(require_mode.is_module_folder_name(Path::new("init.luau"))); + assert!(require_mode + .is_module_folder_name(Path::new("init.luau")) + .unwrap()); } #[test] fn is_true_for_folder_init_lua() { let require_mode = LuauRequireMode::default(); - assert!(require_mode.is_module_folder_name(Path::new("folder/init.lua"))); + assert!(require_mode + .is_module_folder_name(Path::new("folder/init.lua")) + .unwrap()); } #[test] fn is_true_for_folder_init_luau() { let require_mode = LuauRequireMode::default(); - assert!(require_mode.is_module_folder_name(Path::new("folder/init.luau"))); + assert!(require_mode + .is_module_folder_name(Path::new("folder/init.luau")) + .unwrap()); } } diff --git a/src/rules/require/mod.rs b/src/rules/require/mod.rs index 8feb1990..a85213a9 100644 --- a/src/rules/require/mod.rs +++ b/src/rules/require/mod.rs @@ -1,3 +1,4 @@ +mod hybrid_path_locator; mod luau_path_locator; mod luau_require_mode; mod match_require; @@ -5,18 +6,27 @@ mod path_iterator; mod path_locator; mod path_require_mode; pub(crate) mod path_utils; +mod roblox_path_locator; use std::path::{Path, PathBuf}; +pub use crate::rules::require::hybrid_path_locator::SingularPathLocator; +use crate::{nodes::FunctionCall, DarkluaError}; +pub(crate) use hybrid_path_locator::HybridPathLocator; pub(crate) use luau_path_locator::LuauPathLocator; pub use luau_require_mode::LuauRequireMode; pub(crate) use match_require::{is_require_call, match_path_require_call}; pub(crate) use path_locator::RequirePathLocator; pub use path_require_mode::PathRequireMode; - -use crate::DarkluaError; +pub(crate) use roblox_path_locator::RobloxPathLocator; pub(crate) trait PathLocator { + fn match_path_require_call( + &self, + call: &FunctionCall, + _source: &Path, + ) -> Option<(PathBuf, SingularPathLocator<'_, '_, '_>)>; + fn find_require_path( &self, path: impl Into, diff --git a/src/rules/require/path_locator.rs b/src/rules/require/path_locator.rs index f7a5d6ee..3a73d536 100644 --- a/src/rules/require/path_locator.rs +++ b/src/rules/require/path_locator.rs @@ -1,9 +1,15 @@ use std::path::{Path, PathBuf}; use super::{path_iterator, PathRequireMode}; -use crate::{rules::require::path_utils::is_require_relative, utils, DarkluaError, Resources}; +use crate::{ + rules::require::{ + hybrid_path_locator::SingularPathLocator, match_path_require_call, + path_utils::is_require_relative, + }, + utils, DarkluaError, Resources, +}; -#[derive(Debug)] +#[derive(Clone, Debug)] pub(crate) struct RequirePathLocator<'a, 'b, 'resources> { path_require_mode: &'a PathRequireMode, extra_module_relative_location: &'b Path, @@ -25,6 +31,14 @@ impl<'a, 'b, 'c> RequirePathLocator<'a, 'b, 'c> { } impl super::PathLocator for RequirePathLocator<'_, '_, '_> { + fn match_path_require_call( + &self, + call: &crate::nodes::FunctionCall, + _source: &Path, + ) -> Option<(PathBuf, SingularPathLocator<'_, '_, '_>)> { + match_path_require_call(call).map(|x| (x, SingularPathLocator::Path(self.clone()))) + } + fn find_require_path( &self, path: impl Into, diff --git a/src/rules/require/path_require_mode.rs b/src/rules/require/path_require_mode.rs index 093a7917..80a11d65 100644 --- a/src/rules/require/path_require_mode.rs +++ b/src/rules/require/path_require_mode.rs @@ -4,7 +4,7 @@ use crate::frontend::DarkluaResult; use crate::nodes::{Arguments, FunctionCall, StringExpression}; use crate::rules::require::path_utils::get_relative_path; use crate::rules::require::{match_path_require_call, path_utils, PathLocator}; -use crate::rules::{Context, RequireMode}; +use crate::rules::{Context, RequireModeLike, SingularRequireMode}; use crate::utils; use crate::DarkluaError; @@ -57,18 +57,8 @@ fn is_default_module_folder_name(value: &String) -> bool { value == DEFAULT_MODULE_FOLDER_NAME } -impl PathRequireMode { - /// Creates a new path require mode with the specified module folder name. - pub fn new(module_folder_name: impl Into) -> Self { - Self { - module_folder_name: module_folder_name.into(), - sources: Default::default(), - use_luau_configuration: default_use_luau_configuration(), - luau_rc_aliases: Default::default(), - } - } - - pub(crate) fn initialize(&mut self, context: &Context) -> Result<(), DarkluaError> { +impl RequireModeLike for PathRequireMode { + fn initialize(&mut self, context: &Context) -> Result<(), DarkluaError> { if !self.use_luau_configuration { self.luau_rc_aliases.take(); return Ok(()); @@ -85,54 +75,35 @@ impl PathRequireMode { Ok(()) } - pub(crate) fn module_folder_name(&self) -> &str { - &self.module_folder_name - } - - pub(crate) fn get_source(&self, name: &str, rel: &Path) -> Option { - log::trace!( - "lookup alias `{}` from `{}` (path mode)", - name, - rel.display() - ); - - self.sources - .get(name) - .map(|alias| rel.join(alias)) - .or_else(|| { - self.luau_rc_aliases - .as_ref() - .and_then(|aliases| aliases.get(name)) - .map(ToOwned::to_owned) - }) - } - - pub(crate) fn find_require( + fn find_require( &self, call: &FunctionCall, context: &Context, - ) -> DarkluaResult> { + ) -> DarkluaResult> { if let Some(literal_path) = match_path_require_call(call) { let required_path = RequirePathLocator::new(self, context.project_location(), context.resources()) .find_require_path(literal_path, context.current_path())?; - Ok(Some(required_path)) + Ok(Some(( + required_path, + SingularRequireMode::Path(self.clone()), + ))) } else { Ok(None) } } - pub(crate) fn is_module_folder_name(&self, path: &Path) -> bool { + fn is_module_folder_name(&self, path: &Path) -> DarkluaResult { let expect_value = Some(self.module_folder_name.as_str()); - path.file_name().and_then(OsStr::to_str) == expect_value - || path.file_stem().and_then(OsStr::to_str) == expect_value + Ok(path.file_name().and_then(OsStr::to_str) == expect_value + || path.file_stem().and_then(OsStr::to_str) == expect_value) } - pub(crate) fn generate_require( + fn generate_require( &self, require_path: &Path, - _current: &RequireMode, + _current: &T, context: &Context<'_, '_, '_>, ) -> Result, crate::DarkluaError> { let source_path = utils::normalize_path(context.current_path()); @@ -203,7 +174,7 @@ impl PathRequireMode { } }; - if self.is_module_folder_name(&generated_path) { + if self.is_module_folder_name(&generated_path)? { generated_path.pop(); } else if matches!(generated_path.extension(), Some(extension) if extension == "lua" || extension == "luau") { @@ -214,6 +185,40 @@ impl PathRequireMode { } } +impl PathRequireMode { + /// Creates a new path require mode with the specified module folder name. + pub fn new(module_folder_name: impl Into) -> Self { + Self { + module_folder_name: module_folder_name.into(), + sources: Default::default(), + use_luau_configuration: default_use_luau_configuration(), + luau_rc_aliases: Default::default(), + } + } + + pub(crate) fn module_folder_name(&self) -> &str { + &self.module_folder_name + } + + pub(crate) fn get_source(&self, name: &str, rel: &Path) -> Option { + log::trace!( + "lookup alias `{}` from `{}` (path mode)", + name, + rel.display() + ); + + self.sources + .get(name) + .map(|alias| rel.join(alias)) + .or_else(|| { + self.luau_rc_aliases + .as_ref() + .and_then(|aliases| aliases.get(name)) + .map(ToOwned::to_owned) + }) + } +} + fn generate_require_arguments(value: String) -> Option { Some(Arguments::default().with_argument(StringExpression::from_value(value))) } @@ -229,35 +234,45 @@ mod test { fn default_mode_is_false_for_regular_name() { let require_mode = PathRequireMode::default(); - assert!(!require_mode.is_module_folder_name(Path::new("oops.lua"))); + assert!(!require_mode + .is_module_folder_name(Path::new("oops.lua")) + .unwrap()); } #[test] fn default_mode_is_true_for_init_lua() { let require_mode = PathRequireMode::default(); - assert!(require_mode.is_module_folder_name(Path::new("init.lua"))); + assert!(require_mode + .is_module_folder_name(Path::new("init.lua")) + .unwrap()); } #[test] fn default_mode_is_true_for_init_luau() { let require_mode = PathRequireMode::default(); - assert!(require_mode.is_module_folder_name(Path::new("init.luau"))); + assert!(require_mode + .is_module_folder_name(Path::new("init.luau")) + .unwrap()); } #[test] fn default_mode_is_true_for_folder_init_lua() { let require_mode = PathRequireMode::default(); - assert!(require_mode.is_module_folder_name(Path::new("folder/init.lua"))); + assert!(require_mode + .is_module_folder_name(Path::new("folder/init.lua")) + .unwrap()); } #[test] fn default_mode_is_true_for_folder_init_luau() { let require_mode = PathRequireMode::default(); - assert!(require_mode.is_module_folder_name(Path::new("folder/init.luau"))); + assert!(require_mode + .is_module_folder_name(Path::new("folder/init.luau")) + .unwrap()); } } } diff --git a/src/rules/require/roblox_path_locator.rs b/src/rules/require/roblox_path_locator.rs new file mode 100644 index 00000000..9a76d02e --- /dev/null +++ b/src/rules/require/roblox_path_locator.rs @@ -0,0 +1,90 @@ +use std::path::{Path, PathBuf}; + +use crate::{ + nodes::FunctionCall, + rules::{ + parse_roblox, + require::{ + hybrid_path_locator::SingularPathLocator, path_iterator, + path_utils::is_require_relative, PathLocator, + }, + RobloxRequireMode, + }, + utils, DarkluaError, Resources, +}; + +#[derive(Clone, Debug)] +pub(crate) struct RobloxPathLocator<'a, 'b, 'resources> { + _roblox_require_mode: &'a RobloxRequireMode, + _extra_module_relative_location: &'b Path, + resources: &'resources Resources, +} + +impl<'a, 'b, 'c> RobloxPathLocator<'a, 'b, 'c> { + pub(crate) fn new( + roblox_require_mode: &'a RobloxRequireMode, + extra_module_relative_location: &'b Path, + resources: &'c Resources, + ) -> Self { + Self { + _roblox_require_mode: roblox_require_mode, + _extra_module_relative_location: extra_module_relative_location, + resources, + } + } +} + +impl PathLocator for RobloxPathLocator<'_, '_, '_> { + fn match_path_require_call( + &self, + call: &FunctionCall, + source: &Path, + ) -> Option<(PathBuf, SingularPathLocator<'_, '_, '_>)> { + parse_roblox(call, source) + .ok() + .flatten() + .and_then(|x| { + let mut source_parent = source.to_path_buf(); + source_parent.pop(); + pathdiff::diff_paths(x, source_parent).map(|x| PathBuf::from("./").join(x)) + }) + .map(|x| (x, SingularPathLocator::Roblox(self.clone()))) + } + + fn find_require_path( + &self, + path: impl Into, + source: &Path, + ) -> Result { + let mut path: PathBuf = path.into(); + log::trace!( + "find require path for `{}` from `{}`", + path.display(), + source.display() + ); + + if is_require_relative(&path) { + let mut new_path = source.to_path_buf(); + new_path.pop(); + new_path.push(path); + path = new_path; + } + + let normalized_path = utils::normalize_path_with_current_dir(&path); + for potential_path in path_iterator::find_require_paths(&normalized_path, "init") { + if self.resources.is_file(&potential_path)? { + return Ok(utils::normalize_path_with_current_dir(potential_path)); + } + } + + Err( + DarkluaError::resource_not_found(&normalized_path).context(format!( + "tried `{}`", + path_iterator::find_require_paths(&normalized_path, "init") + .map(|potential_path| potential_path.display().to_string()) + .collect::>() + .join("`, `") + )), + ) + } +} diff --git a/src/rules/rule_property.rs b/src/rules/rule_property.rs index 412c7344..21c46579 100644 --- a/src/rules/rule_property.rs +++ b/src/rules/rule_property.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, str::FromStr}; use regex::Regex; use serde::{Deserialize, Serialize}; @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::{ nodes::{DecimalNumber, Expression, StringExpression, TableEntry, TableExpression}, process::to_expression, + rules::SingularRequireMode, }; use super::{ @@ -83,14 +84,12 @@ impl RulePropertyValue { ) -> Result { match self { Self::RequireMode(require_mode) => Ok(require_mode), - Self::String(value) => { - value - .parse() - .map_err(|err: String| RuleConfigurationError::UnexpectedValue { - property: key.to_owned(), - message: err, - }) - } + Self::String(value) => SingularRequireMode::from_str(&value) + .map(RequireMode::Single) + .map_err(|err: String| RuleConfigurationError::UnexpectedValue { + property: key.to_owned(), + message: err, + }), _ => Err(RuleConfigurationError::RequireModeExpected(key.to_owned())), } } @@ -156,27 +155,27 @@ impl From for RulePropertyValue { } } -impl From<&RequireMode> for RulePropertyValue { - fn from(value: &RequireMode) -> Self { +impl From<&SingularRequireMode> for RulePropertyValue { + fn from(value: &SingularRequireMode) -> Self { match value { - RequireMode::Path(mode) => { + SingularRequireMode::Path(mode) => { if mode == &PathRequireMode::default() { return Self::from("path"); } } - RequireMode::Luau(mode) => { + SingularRequireMode::Luau(mode) => { if mode == &LuauRequireMode::default() { return Self::from("luau"); } } - RequireMode::Roblox(mode) => { + SingularRequireMode::Roblox(mode) => { if mode == &RobloxRequireMode::default() { return Self::from("roblox"); } } } - Self::RequireMode(value.clone()) + Self::RequireMode(RequireMode::Single(value.clone())) } } @@ -387,7 +386,9 @@ mod test { fn parse_require_mode_path_object() { parse_rule_property( r#"{"name": "path"}"#, - RulePropertyValue::RequireMode(RequireMode::Path(PathRequireMode::default())), + RulePropertyValue::RequireMode(RequireMode::Single(SingularRequireMode::Path( + PathRequireMode::default(), + ))), ); } @@ -395,7 +396,9 @@ mod test { fn parse_require_mode_path_object_with_options() { parse_rule_property( r#"{"name": "path", "module_folder_name": "index"}"#, - RulePropertyValue::RequireMode(RequireMode::Path(PathRequireMode::new("index"))), + RulePropertyValue::RequireMode(RequireMode::Single(SingularRequireMode::Path( + PathRequireMode::new("index"), + ))), ); } @@ -403,7 +406,9 @@ mod test { fn parse_require_mode_roblox_object() { parse_rule_property( r#"{"name": "roblox"}"#, - RulePropertyValue::RequireMode(RequireMode::Roblox(RobloxRequireMode::default())), + RulePropertyValue::RequireMode(RequireMode::Single(SingularRequireMode::Roblox( + RobloxRequireMode::default(), + ))), ); } @@ -411,12 +416,12 @@ mod test { fn parse_require_mode_roblox_object_with_options() { parse_rule_property( r#"{"name": "roblox", "rojo_sourcemap": "./sourcemap.json"}"#, - RulePropertyValue::RequireMode(RequireMode::Roblox( + RulePropertyValue::RequireMode(RequireMode::Single(SingularRequireMode::Roblox( serde_json::from_str::( r#"{"rojo_sourcemap": "./sourcemap.json"}"#, ) .unwrap(), - )), + ))), ); } @@ -424,7 +429,20 @@ mod test { fn parse_require_mode_luau_object() { parse_rule_property( r#"{ "name": "luau" }"#, - RulePropertyValue::RequireMode(RequireMode::Luau(LuauRequireMode::default())), + RulePropertyValue::RequireMode(RequireMode::Single(SingularRequireMode::Luau( + LuauRequireMode::default(), + ))), + ); + } + + #[test] + fn parse_require_mode_hybrid() { + parse_rule_property( + r#"[{ "name": "luau" }, { "name": "roblox" }]"#, + RulePropertyValue::RequireMode(RequireMode::Hybrid(vec![ + SingularRequireMode::Luau(Default::default()), + SingularRequireMode::Roblox(Default::default()), + ])), ); } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 8978b0c7..b7a13d8e 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -5,6 +5,7 @@ mod luau_config; mod preserve_arguments_side_effects; mod scoped_hash_map; mod serde_one_or_many; +mod serde_string_or_default; mod serde_string_or_struct; mod timer; @@ -14,6 +15,7 @@ pub(crate) use luau_config::{clear_luau_configuration_cache, find_luau_configura pub(crate) use preserve_arguments_side_effects::preserve_arguments_side_effects; pub(crate) use scoped_hash_map::ScopedHashMap; pub(crate) use serde_one_or_many::deserialize_one_or_many; +pub(crate) use serde_string_or_default::string_or_default; pub(crate) use serde_string_or_struct::string_or_struct; use std::{ ffi::OsStr, diff --git a/src/utils/serde_string_or_default.rs b/src/utils/serde_string_or_default.rs new file mode 100644 index 00000000..95538027 --- /dev/null +++ b/src/utils/serde_string_or_default.rs @@ -0,0 +1,51 @@ +use std::{fmt, marker::PhantomData, str::FromStr}; + +use serde::{ + de::{ + value::MapAccessDeserializer, value::SeqAccessDeserializer, MapAccess, SeqAccess, Visitor, + }, + Deserialize, Deserializer, +}; + +pub(crate) fn string_or_default<'de, T, D>(deserializer: D) -> Result +where + T: Deserialize<'de> + FromStr, + D: Deserializer<'de>, +{ + struct StringOrStruct(PhantomData); + + impl<'de, T> Visitor<'de> for StringOrStruct + where + T: Deserialize<'de> + FromStr, + { + type Value = T; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string or object") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + T::from_str(value).map_err(E::custom) + } + + fn visit_map(self, map: M) -> Result + where + M: MapAccess<'de>, + { + Deserialize::deserialize(MapAccessDeserializer::new(map)) + } + + fn visit_seq(self, seq: S) -> Result + where + S: SeqAccess<'de>, + { + // Delegate to the type's standard deserializer for sequences + Deserialize::deserialize(SeqAccessDeserializer::new(seq)) + } + } + + deserializer.deserialize_any(StringOrStruct(PhantomData)) +}