From 110eb731da46f75871deba2cc934b529dd128f28 Mon Sep 17 00:00:00 2001 From: Chris Rybicki Date: Wed, 11 Oct 2023 12:26:19 -0400 Subject: [PATCH] fix(compiler)!: extern methods are not resolved correctly (#4043) Previously, the compiler resolved strings in `extern` methods by performing a lookup, starting at the project's root directory. However, in multi-file projects this gives incorrect results since the source file might be inside a subdirectory of the user's project (or in the case of Wing libraries, inside their `node_modules` folder), resulting in bugs like #3916. To fix this, we now resolve all paths into absolute paths while we're parsing all of the files, so that absolute paths are stored in most places in the compiler (including e.g. bring statement and extern statement AST nodes). Fixes #3916 Fixes #4431 BREAKING CHANGE: `extern` statements can no longer refer to npm libraries, like `extern "uuid" pub static v4(): str;`. Instead, you can import the library in a JavaScript file and use the functionality through a regular export: ```js // main.w class Helpers { extern "./util.js" static v4(): str; } // util.js exports.v4 = function() { return require("uuid").v4(); }; ``` ## Checklist - [x] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted) - [x] Description explains motivation and solution - [x] Tests added (always) - [ ] Docs updated (only required for features) - [x] Added `pr/e2e-full` label if this feature requires end-to-end testing *By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*. --- .../server/src/utils/format-wing-error.ts | 15 ++-- apps/wing/src/commands/compile.ts | 15 ++-- docs/docs/03-language-reference.md | 5 +- examples/tests/invalid/extern.test.w | 1 - examples/tests/invalid/extern_static.test.w | 2 +- .../tests/valid/bring_wing_library.test.w | 6 ++ .../tests/valid/extern_implementation.test.w | 2 - examples/tests/valid/subdir/subfile.w | 4 +- examples/tests/valid/subdir/util.js | 3 + examples/wing-fixture/store.w | 3 + examples/wing-fixture/util.js | 5 ++ libs/wingc/examples/compile.rs | 2 +- libs/wingc/src/diagnostic.rs | 11 ++- libs/wingc/src/jsify.rs | 28 +----- libs/wingc/src/lib.rs | 36 ++++---- libs/wingc/src/lsp/sync.rs | 11 +-- libs/wingc/src/parser.rs | 8 +- libs/wingc/src/test_utils.rs | 12 +-- libs/wingc/src/type_check.rs | 2 +- libs/wingcompiler/src/compile.ts | 12 +-- tools/hangar/__snapshots__/invalid.ts.snap | 87 +++++++++---------- .../bring_local.test.w_compile_tf-aws.md | 5 +- ...ring_wing_library.test.w_compile_tf-aws.md | 61 ++++++++++++- .../bring_wing_library.test.w_test_sim.md | 2 +- .../valid/debug_env.test.w_test_sim.md | 2 +- ...rn_implementation.test.w_compile_tf-aws.md | 4 - tools/hangar/src/invalid.test.ts | 9 +- 27 files changed, 199 insertions(+), 154 deletions(-) create mode 100644 examples/tests/valid/subdir/util.js create mode 100644 examples/wing-fixture/util.js diff --git a/apps/wing-console/console/server/src/utils/format-wing-error.ts b/apps/wing-console/console/server/src/utils/format-wing-error.ts index 2206d6ca33e..8fc82ac06a5 100644 --- a/apps/wing-console/console/server/src/utils/format-wing-error.ts +++ b/apps/wing-console/console/server/src/utils/format-wing-error.ts @@ -43,8 +43,9 @@ export const formatWingError = async (error: unknown) => { for (const error of errors) { const { message, span, annotations } = error; - let files: File[] = []; - let labels: Label[] = []; + const files: File[] = []; + const labels: Label[] = []; + const cwd = process.cwd(); // file_id might be "" if the span is synthetic (see #2521) if (span !== null && span !== undefined && span.file_id) { @@ -60,9 +61,10 @@ export const formatWingError = async (error: unknown) => { span.end.line, span.end.col, ); - files.push({ name: span.file_id, source }); + const filePath = relative(cwd, span.file_id); + files.push({ name: filePath, source }); labels.push({ - fileId: span.file_id, + fileId: filePath, rangeStart: start, rangeEnd: end, message, @@ -82,9 +84,10 @@ export const formatWingError = async (error: unknown) => { annotation.span.end.line, annotation.span.end.col, ); - files.push({ name: annotation.span.file_id, source }); + const filePath = relative(cwd, annotation.span.file_id); + files.push({ name: filePath, source }); labels.push({ - fileId: annotation.span.file_id, + fileId: filePath, rangeStart: start, rangeEnd: end, message: annotation.message, diff --git a/apps/wing/src/commands/compile.ts b/apps/wing/src/commands/compile.ts index 620a0068114..f90f1808d87 100644 --- a/apps/wing/src/commands/compile.ts +++ b/apps/wing/src/commands/compile.ts @@ -94,12 +94,13 @@ export async function compile(entrypoint?: string, options?: CompileOptions): Pr if (error instanceof wingCompiler.CompileError) { // This is a bug in the user's code. Print the compiler diagnostics. const diagnostics = error.diagnostics; + const cwd = process.cwd(); const result = []; for (const diagnostic of diagnostics) { const { message, span, annotations } = diagnostic; - let files: File[] = []; - let labels: Label[] = []; + const files: File[] = []; + const labels: Label[] = []; // file_id might be "" if the span is synthetic (see #2521) if (span?.file_id) { @@ -107,9 +108,10 @@ export async function compile(entrypoint?: string, options?: CompileOptions): Pr const source = await fsPromise.readFile(span.file_id, "utf8"); const start = byteOffsetFromLineAndColumn(source, span.start.line, span.start.col); const end = byteOffsetFromLineAndColumn(source, span.end.line, span.end.col); - files.push({ name: span.file_id, source }); + const filePath = relative(cwd, span.file_id); + files.push({ name: filePath, source }); labels.push({ - fileId: span.file_id, + fileId: filePath, rangeStart: start, rangeEnd: end, message, @@ -129,9 +131,10 @@ export async function compile(entrypoint?: string, options?: CompileOptions): Pr annotation.span.end.line, annotation.span.end.col ); - files.push({ name: annotation.span.file_id, source }); + const filePath = relative(cwd, annotation.span.file_id); + files.push({ name: filePath, source }); labels.push({ - fileId: annotation.span.file_id, + fileId: filePath, rangeStart: start, rangeEnd: end, message: annotation.message, diff --git a/docs/docs/03-language-reference.md b/docs/docs/03-language-reference.md index d522f445c45..ea9dd8f17b9 100644 --- a/docs/docs/03-language-reference.md +++ b/docs/docs/03-language-reference.md @@ -1840,7 +1840,7 @@ let bucket = new awscdk.aws_s3.Bucket( ## 5.2 JavaScript -The `extern ""` modifier can be used on method declarations in classes to indicate that a method is backed by an implementation imported from a JavaScript module. The module can either be a relative path or a name and will be loaded via [require()](https://nodejs.org/api/modules.html#requireid). +The `extern ""` modifier can be used on method declarations in classes to indicate that a method is backed by an implementation imported from a JavaScript module. The module must be a relative path and will be loaded via [require()](https://nodejs.org/api/modules.html#requireid). In the following example, the static inflight method `makeId` is implemented in `helper.js`: @@ -1860,9 +1860,6 @@ class TaskList { // Load js helper file extern "./helpers.js" static inflight makeId(): str; - - // Alternatively, you can use a module name - extern "uuid" static inflight v4(): str; } // helpers.js diff --git a/examples/tests/invalid/extern.test.w b/examples/tests/invalid/extern.test.w index 5094dd32f60..69ff9a4fc5f 100644 --- a/examples/tests/invalid/extern.test.w +++ b/examples/tests/invalid/extern.test.w @@ -1,5 +1,4 @@ class Foo { extern "./sad.js" static getNum(): num; //^ "./sad.js" not found - extern "not-installed" static tooBad(): bool; } diff --git a/examples/tests/invalid/extern_static.test.w b/examples/tests/invalid/extern_static.test.w index 8c59090ea22..8efcc58d8b2 100644 --- a/examples/tests/invalid/extern_static.test.w +++ b/examples/tests/invalid/extern_static.test.w @@ -1,4 +1,4 @@ class Foo { - extern "../external_js.js" inflight getGreeting(name: str): str; + extern "../valid/external_js.js" inflight getGreeting(name: str): str; //^ Error: extern methods must be declared "static" } diff --git a/examples/tests/valid/bring_wing_library.test.w b/examples/tests/valid/bring_wing_library.test.w index 4fd665e57ac..66b46bdcf79 100644 --- a/examples/tests/valid/bring_wing_library.test.w +++ b/examples/tests/valid/bring_wing_library.test.w @@ -2,3 +2,9 @@ bring "wing-fixture" as fixture; new fixture.Store(); let fave_num = fixture.FavoriteNumbers.SEVEN; + +assert(fixture.Store.makeKey("hello") == "data/hello.json"); + +test "makeKeyInflight" { + assert(fixture.Store.makeKeyInflight("hello") == "data/hello.json"); +} diff --git a/examples/tests/valid/extern_implementation.test.w b/examples/tests/valid/extern_implementation.test.w index c4da26f6e79..24143e3f6b7 100644 --- a/examples/tests/valid/extern_implementation.test.w +++ b/examples/tests/valid/extern_implementation.test.w @@ -6,7 +6,6 @@ class Foo { extern "./external_js.js" static inflight getUuid(): str; extern "./external_js.js" static inflight getData(): str; extern "./external_js.js" pub static inflight print(msg: str); - extern "uuid" pub static v4(): str; pub inflight call() { assert(Foo.regexInflight("[a-z]+-\\d+", "abc-123")); @@ -18,7 +17,6 @@ class Foo { } assert(Foo.getGreeting("Wingding") == "Hello, Wingding!"); -assert(Foo.v4().length == 36); let f = new Foo(); diff --git a/examples/tests/valid/subdir/subfile.w b/examples/tests/valid/subdir/subfile.w index 12f7325ce27..def77857234 100644 --- a/examples/tests/valid/subdir/subfile.w +++ b/examples/tests/valid/subdir/subfile.w @@ -1,3 +1,5 @@ bring math; -class Q {} +class Q { + extern "./util.js" static inflight greet(name: str): str; +} diff --git a/examples/tests/valid/subdir/util.js b/examples/tests/valid/subdir/util.js new file mode 100644 index 00000000000..9613a35374a --- /dev/null +++ b/examples/tests/valid/subdir/util.js @@ -0,0 +1,3 @@ +exports.greet = function(name) { + return 'Hello ' + name; +} diff --git a/examples/wing-fixture/store.w b/examples/wing-fixture/store.w index 098981cda7c..844a4b6493e 100644 --- a/examples/wing-fixture/store.w +++ b/examples/wing-fixture/store.w @@ -7,6 +7,9 @@ class Store { this.data = new cloud.Bucket(); } + extern "./util.js" pub static makeKey(name: str): str; + extern "./util.js" pub static inflight makeKeyInflight(name: str): str; + inflight set(message: str) { this.data.put("data.txt", myutil.double(message)); } diff --git a/examples/wing-fixture/util.js b/examples/wing-fixture/util.js new file mode 100644 index 00000000000..fd7d5bf919b --- /dev/null +++ b/examples/wing-fixture/util.js @@ -0,0 +1,5 @@ +exports.makeKey = function(name) { + return "data/" + name + ".json"; +} + +exports.makeKeyInflight = exports.makeKey; diff --git a/libs/wingc/examples/compile.rs b/libs/wingc/examples/compile.rs index 22874549cc1..d9c96cd6ce7 100644 --- a/libs/wingc/examples/compile.rs +++ b/libs/wingc/examples/compile.rs @@ -15,7 +15,7 @@ pub fn main() { let source_path = Utf8Path::new(&args[1]).canonicalize_utf8().unwrap(); let target_dir: Utf8PathBuf = env::current_dir().unwrap().join("target").try_into().unwrap(); - let results = compile(&source_path, None, &target_dir, source_path.parent().unwrap()); + let results = compile(source_path.parent().unwrap(), &source_path, None, &target_dir); if results.is_err() { let mut diags = get_diagnostics(); // Sort error messages by line number (ascending) diff --git a/libs/wingc/src/diagnostic.rs b/libs/wingc/src/diagnostic.rs index 37a9e766d1a..e415612f569 100644 --- a/libs/wingc/src/diagnostic.rs +++ b/libs/wingc/src/diagnostic.rs @@ -1,3 +1,4 @@ +use camino::Utf8Path; use colored::Colorize; use std::{cell::RefCell, fmt::Display}; use tree_sitter::Point; @@ -205,9 +206,15 @@ impl WingSpan { } } -impl std::fmt::Display for WingSpan { +impl Display for WingSpan { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}:{}:{}", self.file_id, self.start.line + 1, self.start.col + 1) + write!( + f, + "{}:{}:{}", + Utf8Path::new(&self.file_id).file_name().expect("invalid file id"), + self.start.line + 1, + self.start.col + 1 + ) } } diff --git a/libs/wingc/src/jsify.rs b/libs/wingc/src/jsify.rs index b3ebf552623..f2e7500be3e 100644 --- a/libs/wingc/src/jsify.rs +++ b/libs/wingc/src/jsify.rs @@ -15,7 +15,7 @@ use crate::{ StmtKind, Symbol, UnaryOperator, UserDefinedType, }, comp_ctx::{CompilationContext, CompilationPhase}, - dbg_panic, debug, + dbg_panic, diagnostic::{report_diagnostic, Diagnostic, WingSpan}, file_graph::FileGraph, files::Files, @@ -71,8 +71,6 @@ pub struct JSifier<'a> { preflight_file_map: RefCell>, source_files: &'a Files, source_file_graph: &'a FileGraph, - /// Root of the project, used for resolving extern modules - absolute_project_root: &'a Utf8Path, /// The path that compilation started at (file or directory) compilation_init_path: &'a Utf8Path, } @@ -91,7 +89,6 @@ impl<'a> JSifier<'a> { source_files: &'a Files, source_file_graph: &'a FileGraph, compilation_init_path: &'a Utf8Path, - absolute_project_root: &'a Utf8Path, ) -> Self { let output_files = Files::default(); Self { @@ -99,7 +96,6 @@ impl<'a> JSifier<'a> { source_files, source_file_graph, compilation_init_path, - absolute_project_root, referenced_struct_schemas: RefCell::new(BTreeMap::new()), inflight_file_counter: RefCell::new(0), inflight_file_map: RefCell::new(IndexMap::new()), @@ -1083,26 +1079,8 @@ impl<'a> JSifier<'a> { code.add_code(self.jsify_scope_body(scope, ctx)); code } - FunctionBody::External(external_spec) => { - debug!( - "Resolving extern \"{}\" from \"{}\"", - external_spec, self.absolute_project_root - ); - let resolved_path = - match wingii::node_resolve::resolve_from(&external_spec, Utf8Path::new(&self.absolute_project_root)) { - Ok(resolved_path) => resolved_path.to_string().replace("\\", "/"), - Err(err) => { - report_diagnostic(Diagnostic { - message: format!("Failed to resolve extern \"{external_spec}\": {err}"), - span: Some(func_def.span.clone()), - annotations: vec![], - }); - format!("/* unresolved: \"{external_spec}\" */") - } - }; - CodeMaker::one_line(format!( - "return (require(\"{resolved_path}\")[\"{name}\"])({parameters})" - )) + FunctionBody::External(file_path) => { + CodeMaker::one_line(format!("return (require(\"{file_path}\")[\"{name}\"])({parameters})")) } }; let mut prefix = vec![]; diff --git a/libs/wingc/src/lib.rs b/libs/wingc/src/lib.rs index 187a64cafd8..224c1e7a76c 100644 --- a/libs/wingc/src/lib.rs +++ b/libs/wingc/src/lib.rs @@ -171,8 +171,11 @@ pub unsafe extern "C" fn wingc_compile(ptr: u32, len: u32) -> u64 { return WASM_RETURN_ERROR; } let source_path = Utf8Path::new(split[0]); - let output_dir = split.get(1).map(|s| Utf8Path::new(s)).unwrap(); - let absolute_project_dir = split.get(2).map(|s| Utf8Path::new(s)).unwrap(); + let output_dir = split.get(1).map(|s| Utf8Path::new(s)).expect("output dir not provided"); + let project_dir = split + .get(2) + .map(|s| Utf8Path::new(s)) + .expect("project dir not provided"); if !source_path.exists() { report_diagnostic(Diagnostic { @@ -183,7 +186,7 @@ pub unsafe extern "C" fn wingc_compile(ptr: u32, len: u32) -> u64 { return WASM_RETURN_ERROR; } - let results = compile(source_path, None, output_dir, absolute_project_dir); + let results = compile(project_dir, source_path, None, output_dir); if results.is_err() { WASM_RETURN_ERROR } else { @@ -287,10 +290,10 @@ fn add_builtin(name: &str, typ: Type, scope: &mut Scope, types: &mut Types) { } pub fn compile( + project_dir: &Utf8Path, source_path: &Utf8Path, source_text: Option, out_dir: &Utf8Path, - absolute_project_root: &Utf8Path, ) -> Result { let source_path = normalize_path(source_path, None); @@ -357,8 +360,6 @@ pub fn compile( asts.insert(file.clone(), scope); } - let project_dir = absolute_project_root; - // Verify that the project dir is absolute if !is_absolute_path(&project_dir) { report_diagnostic(Diagnostic { @@ -369,7 +370,7 @@ pub fn compile( return Err(()); } - let mut jsifier = JSifier::new(&mut types, &files, &file_graph, &source_path, &project_dir); + let mut jsifier = JSifier::new(&mut types, &files, &file_graph, &source_path); // -- LIFTING PHASE -- @@ -435,39 +436,34 @@ pub fn is_absolute_path(path: &Utf8Path) -> bool { #[cfg(test)] mod sanity { - use camino::Utf8PathBuf; + use camino::{Utf8Path, Utf8PathBuf}; use crate::{compile, diagnostic::assert_no_panics}; - use std::{fs, path::Path}; + use std::fs; fn get_wing_files

(dir: P) -> impl Iterator where - P: AsRef, + P: AsRef, { - fs::read_dir(dir) + fs::read_dir(dir.as_ref()) .unwrap() .map(|entry| Utf8PathBuf::from_path_buf(entry.unwrap().path()).expect("invalid unicode path")) .filter(|path| path.is_file() && path.extension().map(|ext| ext == "w").unwrap_or(false)) } fn compile_test(test_dir: &str, expect_failure: bool) { - for test_file in get_wing_files(test_dir) { + let test_dir = Utf8Path::new(test_dir).canonicalize_utf8().unwrap(); + for test_file in get_wing_files(&test_dir) { println!("\n=== {} ===\n", test_file); - let mut out_dir = test_file.parent().unwrap().to_path_buf(); - out_dir.push(format!("target/wingc/{}.out", test_file.file_name().unwrap())); + let out_dir = test_dir.join(format!("target/wingc/{}.out", test_file.file_name().unwrap())); // reset out_dir if out_dir.exists() { fs::remove_dir_all(&out_dir).expect("remove out dir"); } - let result = compile( - &test_file, - None, - &out_dir, - test_file.canonicalize_utf8().unwrap().parent().unwrap(), - ); + let result = compile(&test_dir, &test_file, None, &out_dir); if result.is_err() { assert!( diff --git a/libs/wingc/src/lsp/sync.rs b/libs/wingc/src/lsp/sync.rs index cebf551b7ee..f48489a4e42 100644 --- a/libs/wingc/src/lsp/sync.rs +++ b/libs/wingc/src/lsp/sync.rs @@ -195,16 +195,7 @@ fn partial_compile( // -- LIFTING PHASE -- - // source_file will never be "" because it is the path to the file being compiled and lsp does not allow empty paths - let project_dir = source_path.parent().expect("Empty filename"); - - let jsifier = JSifier::new( - &mut types, - &project_data.files, - &project_data.file_graph, - &source_path, - &project_dir, - ); + let jsifier = JSifier::new(&mut types, &project_data.files, &project_data.file_graph, &source_path); for file in &topo_sorted_files { let mut lift = LiftVisitor::new(&jsifier); let scope = project_data.asts.remove(file).expect("matching AST not found"); diff --git a/libs/wingc/src/parser.rs b/libs/wingc/src/parser.rs index f4de29a965f..7132df046ab 100644 --- a/libs/wingc/src/parser.rs +++ b/libs/wingc/src/parser.rs @@ -1393,8 +1393,12 @@ impl<'s> Parser<'s> { let signature = self.build_function_signature(func_def_node, phase)?; let statements = if let Some(external) = func_def_node.child_by_field_name("extern_modifier") { let node_text = self.node_text(&external.named_child(0).unwrap()); - let node_text = &node_text[1..node_text.len() - 1]; - FunctionBody::External(node_text.to_string()) + let file_path = Utf8Path::new(&node_text[1..node_text.len() - 1]); + let file_path = normalize_path(file_path, Some(&Utf8Path::new(&self.source_name))); + if !file_path.exists() { + self.add_error(format!("File not found: {}", node_text), &external); + } + FunctionBody::External(file_path.to_string()) } else { FunctionBody::Statements(self.build_scope(&self.get_child_field(func_def_node, "block")?, phase)) }; diff --git a/libs/wingc/src/test_utils.rs b/libs/wingc/src/test_utils.rs index 4e55895e0d5..b445b6c07ae 100644 --- a/libs/wingc/src/test_utils.rs +++ b/libs/wingc/src/test_utils.rs @@ -51,10 +51,10 @@ pub fn compile_fail(code: &str) -> String { /// Compiles `code` and returns the capture scanner results as a string that can be snapshotted fn compile_code(code: &str) -> String { - let outdir = tempfile::tempdir().unwrap(); - let outdir_path = Utf8Path::from_path(outdir.path()).unwrap(); - - let source_path = Utf8Path::new("main.w"); + let project_dir = tempfile::tempdir().unwrap(); + let project_dir = Utf8Path::from_path(project_dir.path()).unwrap(); + let source_path = project_dir.join("main.w"); + let out_dir = project_dir.join("target/main.out/.wing"); // NOTE: this is needed for debugging to work regardless of where you run the test env::set_current_dir(env!("CARGO_MANIFEST_DIR")).unwrap(); @@ -62,13 +62,13 @@ fn compile_code(code: &str) -> String { // convert tabs to 2 spaces let code = code.replace("\t", " "); - let result = compile(source_path, Some(code.clone()), outdir_path, outdir_path); + let result = compile(project_dir, &source_path, Some(code.clone()), &out_dir); let mut snap = vec![]; match result { Ok(_) => { - let Ok(files) = read_dir(outdir.path()) else { + let Ok(files) = read_dir(out_dir) else { panic!("failed to read dir"); }; diff --git a/libs/wingc/src/type_check.rs b/libs/wingc/src/type_check.rs index a3f198a25f9..e2d7caa6be6 100644 --- a/libs/wingc/src/type_check.rs +++ b/libs/wingc/src/type_check.rs @@ -2142,7 +2142,7 @@ impl<'a> TypeChecker<'a> { if let CalleeKind::Expr(call_expr) = callee { if let ExprKind::Reference(Reference::Identifier(ident)) = &call_expr.kind { if ident.name == "wingc_env" { - println!("[symbol environment at {}]", exp.span().to_string()); + println!("[symbol environment at {}]", exp.span().file_id); println!("{}", env.to_string()); } } diff --git a/libs/wingcompiler/src/compile.ts b/libs/wingcompiler/src/compile.ts index 321d937a40d..0a8f02dc277 100644 --- a/libs/wingcompiler/src/compile.ts +++ b/libs/wingcompiler/src/compile.ts @@ -99,9 +99,9 @@ export async function compile(entrypoint: string, options: CompileOptions): Prom const { log } = options; // create a unique temporary directory for the compilation const targetdir = options.targetDir ?? join(dirname(entrypoint), "target"); - const wingFile = entrypoint; + const wingFile = resolve(entrypoint); log?.("wing file: %s", wingFile); - const wingDir = dirname(wingFile); + const wingDir = resolve(dirname(wingFile)); log?.("wing dir: %s", wingDir); const testing = options.testing ?? false; log?.("testing: %s", testing); @@ -122,12 +122,12 @@ export async function compile(entrypoint: string, options: CompileOptions): Prom const tempProcess: { env: Record } = { env: { ...process.env } }; - tempProcess.env["WING_SOURCE_DIR"] = resolve(wingDir); + tempProcess.env["WING_SOURCE_DIR"] = wingDir; if (options.rootId) { tempProcess.env["WING_ROOT_ID"] = options.rootId; } // from wingDir, find the nearest node_modules directory - let wingNodeModules = resolve(wingDir, "node_modules"); + let wingNodeModules = join(wingDir, "node_modules"); while (!existsSync(wingNodeModules)) { wingNodeModules = dirname(dirname(wingNodeModules)); @@ -175,7 +175,7 @@ export async function compile(entrypoint: string, options: CompileOptions): Prom errors.push(JSON.parse(data_str)); } - const arg = `${normalPath(wingFile)};${normalPath(workDir)};${normalPath(resolve(wingDir))}`; + const arg = `${normalPath(wingFile)};${normalPath(workDir)};${normalPath(wingDir)}`; log?.(`invoking %s with: "%s"`, WINGC_COMPILE, arg); let compileSuccess: boolean; try { @@ -227,7 +227,7 @@ async function runPreflightCodeInVm( tempProcess: { env: Record }, log?: (...args: any[]) => void ): Promise { - const artifactPath = resolve(workDir, WINGC_PREFLIGHT); + const artifactPath = join(workDir, WINGC_PREFLIGHT); log?.("reading artifact from %s", artifactPath); const artifact = await fs.readFile(artifactPath, "utf-8"); log?.("artifact: %s", artifact); diff --git a/tools/hangar/__snapshots__/invalid.ts.snap b/tools/hangar/__snapshots__/invalid.ts.snap index ed19ecf15b2..142acda8eec 100644 --- a/tools/hangar/__snapshots__/invalid.ts.snap +++ b/tools/hangar/__snapshots__/invalid.ts.snap @@ -4,7 +4,7 @@ exports[`access_hidden_namespace.test.w 1`] = ` "error: Unknown symbol \\"core\\" --> ../../../examples/tests/invalid/access_hidden_namespace.test.w:7:5 | -7 | new core.NodeJsCode(\\"/tmp/test.txt\\"); // This should fail even though \`fs.TextFile\` extends \`core.FileBase\` because we didn't bring in \`core\` explicitly. +7 | new core.NodeJsCode(\\"/test.txt\\"); // This should fail even though \`fs.TextFile\` extends \`core.FileBase\` because we didn't bring in \`core\` explicitly. | ^^^^ Unknown symbol \\"core\\" @@ -260,8 +260,8 @@ exports[`ambiguous_api_paths.test.w 1`] = ` ../../../examples/tests/invalid/target/test/ambiguous_api_paths.test.wsim.[REDACTED].tmp/.wing/preflight.js:38 const handler = new $Closure1(this,\\"$Closure1\\"); - (api.get(\\"/test/path\\",handler)); ->> (api.get(\\"/test/{variable}\\",handler)); + (api.get(\\"/path\\",handler)); +>> (api.get(\\"/{variable}\\",handler)); } } @@ -323,11 +323,11 @@ exports[`bring_jsii.test.w 1`] = ` | ^^^^^^^^^^^^^^^^^^^^^^^^^^ bring \\"jsii-code-samples\\" must be assigned to an identifier (e.g. bring \\"foo\\" as foo) -error: Unable to load \\"foobar\\": Module not found in \\"../../../examples/tests/invalid/bring_jsii.test.w\\" +error: Unable to load \\"foobar\\": Module not found in \\"/bring_jsii.test.w\\" --> ../../../examples/tests/invalid/bring_jsii.test.w:4:1 | 4 | bring \\"foobar\\" as baz; - | ^^^^^^^^^^^^^^^^^^^^^^ Unable to load \\"foobar\\": Module not found in \\"../../../examples/tests/invalid/bring_jsii.test.w\\" + | ^^^^^^^^^^^^^^^^^^^^^^ Unable to load \\"foobar\\": Module not found in \\"/bring_jsii.test.w\\" @@ -345,18 +345,18 @@ exports[`bring_local_dir.test.w 1`] = ` | ^^^^^^^^^ Cannot bring \\"/subdir\\" since it is not a relative path -error: Symbol \\"Foo\\" has multiple definitions in \\"../../../examples/tests/invalid/subdir/inner\\" +error: Symbol \\"Foo\\" has multiple definitions in \\"/inner\\" --> ../../../examples/tests/invalid/subdir/other.w:1:1 | 1 | bring \\"./inner\\" as inner; - | ^^^^^^^^^^^^^^^^^^^^^^^^^ Symbol \\"Foo\\" has multiple definitions in \\"../../../examples/tests/invalid/subdir/inner\\" + | ^^^^^^^^^^^^^^^^^^^^^^^^^ Symbol \\"Foo\\" has multiple definitions in \\"/inner\\" -error: Symbol \\"Foo\\" has multiple definitions in \\"../../../examples/tests/invalid/subdir/inner\\" +error: Symbol \\"Foo\\" has multiple definitions in \\"/inner\\" --> ../../../examples/tests/invalid/bring_local_dir.test.w:1:1 | 1 | bring \\"./subdir\\" as subdir; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Symbol \\"Foo\\" has multiple definitions in \\"../../../examples/tests/invalid/subdir/inner\\" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Symbol \\"Foo\\" has multiple definitions in \\"/inner\\" @@ -575,11 +575,11 @@ error: Expected 0 arguments but got 1 | ^^^^^^^^^ Expected 0 arguments but got 1 -error: Expected 0 named arguments for func at ../../../examples/tests/invalid/class.test.w:13:1 +error: Expected 0 named arguments for func at class.test.w:13:1 --> ../../../examples/tests/invalid/class.test.w:13:1 | 13 | new C9(token: \\"1\\"); - | ^^^^^^^^^^^^^^^^^^ Expected 0 named arguments for func at ../../../examples/tests/invalid/class.test.w:13:1 + | ^^^^^^^^^^^^^^^^^^ Expected 0 named arguments for func at class.test.w:13:1 error: Expected 1 positional argument(s) but got 0 @@ -935,24 +935,24 @@ Duration " `; exports[`cyclic_bring1.w 1`] = ` -"error: Could not compile \\"../../../examples/tests/invalid/cyclic_bring1.w\\" due to cyclic bring statements: -- ../../../examples/tests/invalid/cyclic_bring3.w -- ../../../examples/tests/invalid/cyclic_bring2.w -- ../../../examples/tests/invalid/cyclic_bring1.w +"error: Could not compile \\"/cyclic_bring1.w\\" due to cyclic bring statements: +- /cyclic_bring3.w +- /cyclic_bring2.w +- /cyclic_bring1.w -error: Could not type check \\"../../../examples/tests/invalid/cyclic_bring2.w\\" due to cyclic bring statements +error: Could not type check \\"/cyclic_bring2.w\\" due to cyclic bring statements --> ../../../examples/tests/invalid/cyclic_bring1.w:1:1 | 1 | bring \\"./cyclic_bring2.w\\" as foo; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not type check \\"../../../examples/tests/invalid/cyclic_bring2.w\\" due to cyclic bring statements + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not type check \\"/cyclic_bring2.w\\" due to cyclic bring statements -error: Could not type check \\"../../../examples/tests/invalid/cyclic_bring3.w\\" due to cyclic bring statements +error: Could not type check \\"/cyclic_bring3.w\\" due to cyclic bring statements --> ../../../examples/tests/invalid/cyclic_bring2.w:1:1 | 1 | bring \\"./cyclic_bring3.w\\" as foo; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not type check \\"../../../examples/tests/invalid/cyclic_bring3.w\\" due to cyclic bring statements + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not type check \\"/cyclic_bring3.w\\" due to cyclic bring statements @@ -963,24 +963,24 @@ Duration " `; exports[`cyclic_bring2.w 1`] = ` -"error: Could not compile \\"../../../examples/tests/invalid/cyclic_bring2.w\\" due to cyclic bring statements: -- ../../../examples/tests/invalid/cyclic_bring1.w -- ../../../examples/tests/invalid/cyclic_bring3.w -- ../../../examples/tests/invalid/cyclic_bring2.w +"error: Could not compile \\"/cyclic_bring2.w\\" due to cyclic bring statements: +- /cyclic_bring1.w +- /cyclic_bring3.w +- /cyclic_bring2.w -error: Could not type check \\"../../../examples/tests/invalid/cyclic_bring3.w\\" due to cyclic bring statements +error: Could not type check \\"/cyclic_bring3.w\\" due to cyclic bring statements --> ../../../examples/tests/invalid/cyclic_bring2.w:1:1 | 1 | bring \\"./cyclic_bring3.w\\" as foo; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not type check \\"../../../examples/tests/invalid/cyclic_bring3.w\\" due to cyclic bring statements + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not type check \\"/cyclic_bring3.w\\" due to cyclic bring statements -error: Could not type check \\"../../../examples/tests/invalid/cyclic_bring1.w\\" due to cyclic bring statements +error: Could not type check \\"/cyclic_bring1.w\\" due to cyclic bring statements --> ../../../examples/tests/invalid/cyclic_bring3.w:1:1 | 1 | bring \\"./cyclic_bring1.w\\" as foo; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not type check \\"../../../examples/tests/invalid/cyclic_bring1.w\\" due to cyclic bring statements + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not type check \\"/cyclic_bring1.w\\" due to cyclic bring statements @@ -991,24 +991,24 @@ Duration " `; exports[`cyclic_bring3.w 1`] = ` -"error: Could not compile \\"../../../examples/tests/invalid/cyclic_bring3.w\\" due to cyclic bring statements: -- ../../../examples/tests/invalid/cyclic_bring2.w -- ../../../examples/tests/invalid/cyclic_bring1.w -- ../../../examples/tests/invalid/cyclic_bring3.w +"error: Could not compile \\"/cyclic_bring3.w\\" due to cyclic bring statements: +- /cyclic_bring2.w +- /cyclic_bring1.w +- /cyclic_bring3.w -error: Could not type check \\"../../../examples/tests/invalid/cyclic_bring1.w\\" due to cyclic bring statements +error: Could not type check \\"/cyclic_bring1.w\\" due to cyclic bring statements --> ../../../examples/tests/invalid/cyclic_bring3.w:1:1 | 1 | bring \\"./cyclic_bring1.w\\" as foo; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not type check \\"../../../examples/tests/invalid/cyclic_bring1.w\\" due to cyclic bring statements + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not type check \\"/cyclic_bring1.w\\" due to cyclic bring statements -error: Could not type check \\"../../../examples/tests/invalid/cyclic_bring2.w\\" due to cyclic bring statements +error: Could not type check \\"/cyclic_bring2.w\\" due to cyclic bring statements --> ../../../examples/tests/invalid/cyclic_bring1.w:1:1 | 1 | bring \\"./cyclic_bring2.w\\" as foo; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not type check \\"../../../examples/tests/invalid/cyclic_bring2.w\\" due to cyclic bring statements + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not type check \\"/cyclic_bring2.w\\" due to cyclic bring statements @@ -1056,18 +1056,11 @@ Duration " `; exports[`extern.test.w 1`] = ` -"error: Failed to resolve extern \\"./sad.js\\": Not Found +"error: File not found: \\"./sad.js\\" --> ../../../examples/tests/invalid/extern.test.w:2:3 | 2 | extern \\"./sad.js\\" static getNum(): num; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Failed to resolve extern \\"./sad.js\\": Not Found - - -error: Failed to resolve extern \\"not-installed\\": Not Found - --> ../../../examples/tests/invalid/extern.test.w:4:3 - | -4 | extern \\"not-installed\\" static tooBad(): bool; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Failed to resolve extern \\"not-installed\\": Not Found + | ^^^^^^^^^^^^^^^^^ File not found: \\"./sad.js\\" @@ -1079,10 +1072,10 @@ Duration " exports[`extern_static.test.w 1`] = ` "error: Extern methods must be declared \\"static\\" (they cannot access instance members) - --> ../../../examples/tests/invalid/extern_static.test.w:2:39 + --> ../../../examples/tests/invalid/extern_static.test.w:2:45 | -2 | extern \\"../external_js.js\\" inflight getGreeting(name: str): str; - | ^^^^^^^^^^^ Extern methods must be declared \\"static\\" (they cannot access instance members) +2 | extern \\"../valid/external_js.js\\" inflight getGreeting(name: str): str; + | ^^^^^^^^^^^ Extern methods must be declared \\"static\\" (they cannot access instance members) diff --git a/tools/hangar/__snapshots__/test_corpus/valid/bring_local.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/bring_local.test.w_compile_tf-aws.md index 1ad79f34e98..339f350b855 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/bring_local.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/bring_local.test.w_compile_tf-aws.md @@ -42,6 +42,9 @@ module.exports = function({ }) { class Q { constructor({ }) { } + static async greet(name) { + return (require("/util.js")["greet"])(name) + } } return Q; } @@ -528,7 +531,7 @@ module.exports = function({ $stdlib }) { `; } _getInflightOps() { - return ["$inflight_init"]; + return ["greet", "$inflight_init"]; } } return { Q }; diff --git a/tools/hangar/__snapshots__/test_corpus/valid/bring_wing_library.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/bring_wing_library.test.w_compile_tf-aws.md index 16bee721538..e600e483504 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/bring_wing_library.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/bring_wing_library.test.w_compile_tf-aws.md @@ -1,5 +1,23 @@ # [bring_wing_library.test.w](../../../../../examples/tests/valid/bring_wing_library.test.w) | compile | tf-aws +## inflight.$Closure1-3.js +```js +module.exports = function({ $fixture_Store }) { + class $Closure1 { + constructor({ }) { + const $obj = (...args) => this.handle(...args); + Object.setPrototypeOf($obj, this); + return $obj; + } + async handle() { + {((cond) => {if (!cond) throw new Error("assertion failed: fixture.Store.makeKeyInflight(\"hello\") == \"data/hello.json\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })((await $fixture_Store.makeKeyInflight("hello")),"data/hello.json")))}; + } + } + return $Closure1; +} + +``` + ## inflight.Store-2.js ```js module.exports = function({ $myutil_Util }) { @@ -7,6 +25,9 @@ module.exports = function({ $myutil_Util }) { constructor({ $this_data }) { this.$this_data = $this_data; } + static async makeKeyInflight(name) { + return (require("/util.js")["makeKeyInflight"])(name) + } async set(message) { (await this.$this_data.put("data.txt",(await $myutil_Util.double(message)))); } @@ -104,8 +125,43 @@ const fixture = require("./preflight.wingfixture-5.js")({ $stdlib }); class $Root extends $stdlib.std.Resource { constructor(scope, id) { super(scope, id); + class $Closure1 extends $stdlib.std.Resource { + constructor(scope, id, ) { + super(scope, id); + (std.Node.of(this)).hidden = true; + } + static _toInflightType(context) { + return ` + require("./inflight.$Closure1-3.js")({ + $fixture_Store: ${context._lift($stdlib.core.toLiftableModuleType(fixture.Store, "", "Store"))}, + }) + `; + } + _toInflight() { + return ` + (await (async () => { + const $Closure1Client = ${$Closure1._toInflightType(this)}; + const client = new $Closure1Client({ + }); + if (client.$inflight_init) { await client.$inflight_init(); } + return client; + })()) + `; + } + _getInflightOps() { + return ["handle", "$inflight_init"]; + } + _registerBind(host, ops) { + if (ops.includes("handle")) { + $Closure1._registerBindObject($stdlib.core.toLiftableModuleType(fixture.Store, "", "Store"), host, ["makeKeyInflight"]); + } + super._registerBind(host, ops); + } + } new fixture.Store(this,"fixture.Store"); const fave_num = fixture.FavoriteNumbers.SEVEN; + {((cond) => {if (!cond) throw new Error("assertion failed: fixture.Store.makeKey(\"hello\") == \"data/hello.json\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })((fixture.Store.makeKey("hello")),"data/hello.json")))}; + this.node.root.new("@winglang/sdk.std.Test",std.Test,this,"test:makeKeyInflight",new $Closure1(this,"$Closure1")); } } const $App = $stdlib.core.App.for(process.env.WING_TARGET); @@ -124,6 +180,9 @@ module.exports = function({ $stdlib }) { super(scope, id); this.data = this.node.root.newAbstract("@winglang/sdk.cloud.Bucket",this,"cloud.Bucket"); } + static makeKey(name) { + return (require("/util.js")["makeKey"])(name) + } static _toInflightType(context) { return ` require("./inflight.Store-2.js")({ @@ -144,7 +203,7 @@ module.exports = function({ $stdlib }) { `; } _getInflightOps() { - return ["set", "$inflight_init"]; + return ["makeKeyInflight", "set", "$inflight_init"]; } _registerBind(host, ops) { if (ops.includes("$inflight_init")) { diff --git a/tools/hangar/__snapshots__/test_corpus/valid/bring_wing_library.test.w_test_sim.md b/tools/hangar/__snapshots__/test_corpus/valid/bring_wing_library.test.w_test_sim.md index 54bd5c5e6d9..6886589d5ae 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/bring_wing_library.test.w_test_sim.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/bring_wing_library.test.w_test_sim.md @@ -2,7 +2,7 @@ ## stdout.log ```log -pass โ”€ bring_wing_library.test.wsim (no tests) +pass โ”€ bring_wing_library.test.wsim ยป root/env0/test:makeKeyInflight Tests 1 passed (1) diff --git a/tools/hangar/__snapshots__/test_corpus/valid/debug_env.test.w_test_sim.md b/tools/hangar/__snapshots__/test_corpus/valid/debug_env.test.w_test_sim.md index e23d2c1abc2..831a57bc07d 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/debug_env.test.w_test_sim.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/debug_env.test.w_test_sim.md @@ -2,7 +2,7 @@ ## stdout.log ```log -[symbol environment at ../../../../examples/tests/valid/debug_env.test.w:7:5] +[symbol environment at debug_env.test.w:7:5] level 0: { this => A } level 1: { A => A [type], assert => (condition: bool): void, cloud => cloud [namespace], log => (message: str): void, std => std [namespace], unsafeCast => (value: any): any } pass โ”€ debug_env.test.wsim (no tests) diff --git a/tools/hangar/__snapshots__/test_corpus/valid/extern_implementation.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/extern_implementation.test.w_compile_tf-aws.md index 982ba654ef4..99153a81f5d 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/extern_implementation.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/extern_implementation.test.w_compile_tf-aws.md @@ -116,9 +116,6 @@ class $Root extends $stdlib.std.Resource { static getGreeting(name) { return (require("/external_js.js")["getGreeting"])(name) } - static v4() { - return (require("/index.js")["v4"])() - } static _toInflightType(context) { return ` require("./inflight.Foo-1.js")({ @@ -213,7 +210,6 @@ class $Root extends $stdlib.std.Resource { } } {((cond) => {if (!cond) throw new Error("assertion failed: Foo.getGreeting(\"Wingding\") == \"Hello, Wingding!\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })((Foo.getGreeting("Wingding")),"Hello, Wingding!")))}; - {((cond) => {if (!cond) throw new Error("assertion failed: Foo.v4().length == 36")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })((Foo.v4()).length,36)))}; const f = new Foo(this,"Foo"); this.node.root.new("@winglang/sdk.std.Test",std.Test,this,"test:call",new $Closure1(this,"$Closure1")); this.node.root.new("@winglang/sdk.std.Test",std.Test,this,"test:console",new $Closure2(this,"$Closure2")); diff --git a/tools/hangar/src/invalid.test.ts b/tools/hangar/src/invalid.test.ts index a62ebd82801..ec4025a93a0 100644 --- a/tools/hangar/src/invalid.test.ts +++ b/tools/hangar/src/invalid.test.ts @@ -8,10 +8,8 @@ invalidWingFiles.forEach((wingFile) => { test(wingFile, async ({ expect }) => { const args = ["test", "-t", "sim"]; - const relativeWingFile = path.relative( - tmpDir, - path.join(invalidTestDir, wingFile) - ); + const absoluteWingFile = path.join(invalidTestDir, wingFile); + const relativeWingFile = path.relative(tmpDir, absoluteWingFile); const metaComment = parseMetaCommentFromPath( path.join(invalidTestDir, wingFile) @@ -28,7 +26,8 @@ invalidWingFiles.forEach((wingFile) => { const sanitize = (output: string) => output // Remove absolute paths to wing files - .replaceAll(relativeWingFile, relativeWingFile.replaceAll("\\", "/")) + // .replaceAll(absoluteWingFile, "<") + .replaceAll(/(?<=[\s"])(\/|\w:)\S+\/(.+)/g, "/$2") // Remove absolute paths to source code .replaceAll(/(src\/.+\.rs):\d+:\d+/g, "$1:LINE:COL") // Normalize line endings