Restrict subdirectory imports to main.kcl (#7094)

Signed-off-by: Nick Cameron <nrc@ncameron.org>
This commit is contained in:
Nick Cameron
2025-05-20 18:13:17 +12:00
committed by GitHub
parent 38a245f2fc
commit 651181e62c
6 changed files with 67 additions and 19 deletions

View File

@ -27,9 +27,6 @@ import increment from "util.kcl"
answer = increment(41) answer = increment(41)
``` ```
Imported files _must_ be in the same project so that units are uniform across
modules. This means that it must be in the same directory.
Import statements must be at the top-level of a file. It is not allowed to have Import statements must be at the top-level of a file. It is not allowed to have
an `import` statement inside a function or in the body of an ifelse. an `import` statement inside a function or in the body of an ifelse.
@ -58,6 +55,9 @@ Imported symbols can be renamed for convenience or to avoid name collisions.
import increment as inc, decrement as dec from "util.kcl" import increment as inc, decrement as dec from "util.kcl"
``` ```
You can import files from the current directory or from subdirectories, but if importing from a
subdirectory you can only import `main.kcl`.
--- ---
## Functions vs `clone` ## Functions vs `clone`
@ -229,6 +229,19 @@ The final statement is what's important because it's the return value of the
entire module. The module is expected to return a single object that can be used entire module. The module is expected to return a single object that can be used
as a variable by the file that imports it. as a variable by the file that imports it.
The name of the file or subdirectory is used as the name of the variable within the importing program.
If you want to use a different name, you can do so by using the `as` keyword:
```kcl,norun
import "cube.kcl" // Introduces a new variable called `cube`.
import "cube.kcl" as block // Introduces a new variable called `block`.
import "cube/main.kcl" // Introduces a new variable called `cube`.
import "cube/main.kcl" as block // Introduces a new variable called `block`.
```
If the filename includes hyphens (`-`) or starts with an underscore (`_`), then you must specify a
variable name.
--- ---
## Multiple instances of the same import ## Multiple instances of the same import

View File

@ -1811,14 +1811,25 @@ impl ImportStatement {
match &self.path { match &self.path {
ImportPath::Kcl { filename: s } | ImportPath::Foreign { path: s } => { ImportPath::Kcl { filename: s } | ImportPath::Foreign { path: s } => {
let name = s.file_name().map(|f| f.to_string()); let name = s.to_string_lossy();
if name.ends_with("/main.kcl") || name.ends_with("\\main.kcl") {
let name = &name[..name.len() - 9];
let start = name.rfind(['/', '\\']).map(|s| s + 1).unwrap_or(0);
return Some(name[start..].to_owned());
}
let name = s.file_name().map(|f| f.to_string())?;
if name.contains('\\') || name.contains('/') {
return None;
}
// Remove the extension if it exists. // Remove the extension if it exists.
let extension = s.extension(); let extension = s.extension();
if let Some(extension) = extension { Some(if let Some(extension) = extension {
name.map(|n| n.trim_end_matches(extension).trim_end_matches('.').to_string()) name.trim_end_matches(extension).trim_end_matches('.').to_string()
} else { } else {
name name
} })
} }
ImportPath::Std { path } => path.last().cloned(), ImportPath::Std { path } => path.last().cloned(),
} }
@ -4330,4 +4341,20 @@ startSketchOn(XY)
"# "#
); );
} }
#[test]
fn module_name() {
#[track_caller]
fn assert_mod_name(stmt: &str, name: &str) {
let tokens = crate::parsing::token::lex(stmt, ModuleId::default()).unwrap();
let stmt = crate::parsing::parser::import_stmt(&mut tokens.as_slice()).unwrap();
assert_eq!(stmt.module_name().unwrap(), name);
}
assert_mod_name("import 'foo.kcl'", "foo");
assert_mod_name("import 'foo.kcl' as bar", "bar");
assert_mod_name("import 'main.kcl'", "main");
assert_mod_name("import 'foo/main.kcl'", "foo");
assert_mod_name("import 'foo\\bar\\main.kcl'", "bar");
}
} }

View File

@ -1729,7 +1729,7 @@ fn glob(i: &mut TokenSlice) -> PResult<Token> {
.parse_next(i) .parse_next(i)
} }
fn import_stmt(i: &mut TokenSlice) -> PResult<BoxNode<ImportStatement>> { pub(super) fn import_stmt(i: &mut TokenSlice) -> PResult<BoxNode<ImportStatement>> {
let (visibility, visibility_token) = opt(terminated(item_visibility, whitespace)) let (visibility, visibility_token) = opt(terminated(item_visibility, whitespace))
.parse_next(i)? .parse_next(i)?
.map_or((ItemVisibility::Default, None), |pair| (pair.0, Some(pair.1))); .map_or((ItemVisibility::Default, None), |pair| (pair.0, Some(pair.1)));
@ -1867,7 +1867,7 @@ fn validate_path_string(path_string: String, var_name: bool, path_range: SourceR
return Err(ErrMode::Cut( return Err(ErrMode::Cut(
CompilationError::fatal( CompilationError::fatal(
path_range, path_range,
"import path may only contain alphanumeric characters, underscore, hyphen, and period. KCL files in other directories are not yet supported.", "import path may only contain alphanumeric characters, `_`, `-`, `.`, `/`, and `\\`.",
) )
.into(), .into(),
)); ));
@ -1894,6 +1894,15 @@ fn validate_path_string(path_string: String, var_name: bool, path_range: SourceR
)); ));
} }
if (path_string.contains('/') || path_string.contains('\\'))
&& !(path_string.ends_with("/main.kcl") || path_string.ends_with("\\main.kcl"))
{
return Err(ErrMode::Cut(
CompilationError::fatal(path_range, "import path to a subdirectory must only refer to main.kcl.")
.into(),
));
}
ImportPath::Kcl { ImportPath::Kcl {
filename: TypedPath::new(&path_string), filename: TypedPath::new(&path_string),
} }
@ -4569,9 +4578,14 @@ e
); );
assert_err( assert_err(
r#"import cube from "C:\cube.kcl""#, r#"import cube from "C:\cube.kcl""#,
"import path may only contain alphanumeric characters, underscore, hyphen, and period. KCL files in other directories are not yet supported.", "import path may only contain alphanumeric characters, `_`, `-`, `.`, `/`, and `\\`.",
[17, 30], [17, 30],
); );
assert_err(
r#"import cube from "cube/cube.kcl""#,
"import path to a subdirectory must only refer to main.kcl.",
[17, 32],
);
assert_err( assert_err(
r#"import * as foo from "dsfs""#, r#"import * as foo from "dsfs""#,
"as is not the 'from' keyword", "as is not the 'from' keyword",

View File

@ -14,13 +14,7 @@ description: Result of parsing nested_windows_main_kcl.kcl
}, },
"selector": { "selector": {
"type": "None", "type": "None",
"alias": { "alias": null
"commentStart": 0,
"end": 0,
"name": "bar",
"start": 0,
"type": "Identifier"
}
}, },
"start": 0, "start": 0,
"type": "ImportStatement", "type": "ImportStatement",

View File

@ -1,3 +1,3 @@
import "nested\foo\bar\main.kcl" as bar import "nested\foo\bar\main.kcl"
bar bar

View File

@ -2,6 +2,6 @@
source: kcl-lib/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Result of unparsing nested_windows_main_kcl.kcl description: Result of unparsing nested_windows_main_kcl.kcl
--- ---
import "nested/foo/bar/main.kcl" as bar import "nested/foo/bar/main.kcl"
bar bar