diff --git a/audit.ce b/audit.ce index 33a5d9f3..1dea25d0 100644 --- a/audit.ce +++ b/audit.ce @@ -37,6 +37,7 @@ var total_ok = 0 var total_errors = 0 var total_scripts = 0 var all_failures = [] +var all_unresolved = [] if (target_package) { packages = [target_package] @@ -57,6 +58,12 @@ arrfor(packages, function(p) { arrfor(result.errors, function(e) { push(all_failures, p + ": " + e) }) + + // Check use() resolution + var resolution = shop.audit_use_resolution(p) + arrfor(resolution.unresolved, function(u) { + push(all_unresolved, p + '/' + u.script + ": use('" + u.module + "') cannot be resolved") + }) }) log.console("") @@ -68,7 +75,18 @@ if (length(all_failures) > 0) { log.console("") } -log.console("Audit complete: " + text(total_ok) + "/" + text(total_scripts) + " scripts compiled" + (total_errors > 0 ? ", " + text(total_errors) + " failed" : "")) +if (length(all_unresolved) > 0) { + log.console("Unresolved modules:") + arrfor(all_unresolved, function(u) { + log.console(" " + u) + }) + log.console("") +} + +var summary = "Audit complete: " + text(total_ok) + "/" + text(total_scripts) + " scripts compiled" +if (total_errors > 0) summary = summary + ", " + text(total_errors) + " failed" +if (length(all_unresolved) > 0) summary = summary + ", " + text(length(all_unresolved)) + " unresolved use() calls" +log.console(summary) } run() diff --git a/internal/shop.cm b/internal/shop.cm index 7f2cf8fc..8fdfd63d 100644 --- a/internal/shop.cm +++ b/internal/shop.cm @@ -48,6 +48,9 @@ function hash_path(content, salt) var Shop = {} +// Stack tracking the chain of use() calls for error reporting +var use_stack = [] + var SCOPE_LOCAL = 0 var SCOPE_PACKAGE = 1 var SCOPE_CORE = 2 @@ -1301,6 +1304,8 @@ Shop.use = function use(path, package_context) { var info = resolve_module_info(path, package_context) var _ctx_dir2 = null var _alias2 = null + var _use_entry = path + ' (package: ' + package_context + ')' + var _chain = null if (!info) { log.shop(`Module '${path}' could not be found in package '${package_context}'`) _ctx_dir2 = package_context ? (starts_with(package_context, '/') ? package_context : get_packages_dir() + '/' + fd.safe_package_path(package_context)) : null @@ -1311,13 +1316,32 @@ Shop.use = function use(path, package_context) { _alias2 = pkg_tools.split_alias(package_context, path) if (_alias2 == null && search(path, '/') != null) log.shop(`Alias '${array(path, '/')[0]}' could not be resolved in package '${package_context}'`) + if (length(use_stack) > 0) { + _chain = 'use() chain:' + arrfor(use_stack, function(frame) { _chain = _chain + '\n -> ' + frame }) + _chain = _chain + '\n -> ' + path + ' [NOT FOUND]' + log.error(_chain) + } disrupt } if (use_cache[info.cache_key]) return use_cache[info.cache_key] - use_cache[info.cache_key] = execute_module(info) - return use_cache[info.cache_key] + + push(use_stack, _use_entry) + var _use_result = null + var _use_ok = false + var _load = function() { + _use_result = execute_module(info) + _use_ok = true + } disruption { + pop(use_stack) + disrupt + } + _load() + pop(use_stack) + use_cache[info.cache_key] = _use_result + return _use_result } // Resolve a use() module path to a filesystem path without compiling. @@ -1856,6 +1880,30 @@ function get_package_scripts(package) return scripts } +// Extract use() call arguments from source text. +// Returns an array of literal string arguments found in use('...') calls. +function extract_use_calls(source) { + var uses = [] + var idx = 0 + var start = 0 + var end = 0 + var arg = null + idx = search(source, "use(") + while (idx != null) { + start = idx + 5 + end = search(text(source, start), "'") + if (end == null) end = search(text(source, start), '"') + if (end != null) { + arg = text(source, start, start + end) + push(uses, arg) + } + idx = search(text(source, idx + 4), "use(") + if (idx != null) idx = idx + (source.length - (source.length - idx)) + else break + } + return uses +} + Shop.build_package_scripts = function(package) { // compiles all .ce and .cm files in a package @@ -1879,6 +1927,63 @@ Shop.build_package_scripts = function(package) return {ok: ok, errors: errors, total: length(scripts)} } +// Check if all use() calls in a package's scripts can be resolved. +// Returns {ok, unresolved: [{script, module}], total} +Shop.audit_use_resolution = function(package) { + var scripts = get_package_scripts(package) + var pkg_dir = starts_with(package, '/') ? package : get_package_abs_dir(package) + var unresolved = [] + var checked = 0 + var src = null + var content = null + var uses = null + var info = null + + arrfor(scripts, function(script) { + var _check = function() { + src = pkg_dir + '/' + script + if (!fd.is_file(src)) return + content = text(fd.slurp(src)) + if (!content || length(content) == 0) return + + // Simple regex-free extraction: find use(' and use(" patterns + uses = [] + var pos = 0 + var rest = content + var ui = null + var quote = null + var end = null + var arg = null + while (length(rest) > 0) { + ui = search(rest, "use(") + if (ui == null) break + rest = text(rest, ui + 4) + if (length(rest) == 0) break + quote = text(rest, 0, 1) + if (quote != "'" && quote != '"') continue + rest = text(rest, 1) + end = search(rest, quote) + if (end == null) continue + arg = text(rest, 0, end) + if (length(arg) > 0) push(uses, arg) + rest = text(rest, end + 1) + } + + arrfor(uses, function(mod) { + var _resolve = function() { + info = resolve_module_info(mod, package) + if (!info) push(unresolved, {script: script, module: mod}) + } disruption {} + _resolve() + }) + checked = checked + 1 + } disruption {} + _check() + }) + + return {ok: checked, unresolved: unresolved, total: length(scripts)} +} + Shop.get_package_scripts = get_package_scripts Shop.list_packages = function()