prodex 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +369 -146
  2. package/dist/app/dependency-source-provider.d.ts +2 -0
  3. package/dist/app/dependency-source-provider.js +66 -0
  4. package/dist/app/entry-resolver.d.ts +7 -0
  5. package/dist/app/entry-resolver.js +26 -0
  6. package/dist/app/execute-run.d.ts +2 -2
  7. package/dist/app/execute-run.js +138 -36
  8. package/dist/app/git-source-provider.d.ts +4 -0
  9. package/dist/app/git-source-provider.js +551 -0
  10. package/dist/app/grep-source-provider.d.ts +2 -0
  11. package/dist/app/grep-source-provider.js +226 -0
  12. package/dist/app/planner.d.ts +12 -0
  13. package/dist/app/planner.js +106 -0
  14. package/dist/app/planners/attachment-options.d.ts +2 -0
  15. package/dist/app/planners/attachment-options.js +39 -0
  16. package/dist/app/planners/git-plan.d.ts +13 -0
  17. package/dist/app/planners/git-plan.js +141 -0
  18. package/dist/app/planners/grep-plan.d.ts +13 -0
  19. package/dist/app/planners/grep-plan.js +115 -0
  20. package/dist/app/planners/list-utils.d.ts +1 -0
  21. package/dist/app/planners/list-utils.js +6 -0
  22. package/dist/app/planners/pack-plan.d.ts +13 -0
  23. package/dist/app/planners/pack-plan.js +65 -0
  24. package/dist/app/planners/scope-plan.d.ts +17 -0
  25. package/dist/app/planners/scope-plan.js +143 -0
  26. package/dist/app/planners/trace-plan.d.ts +13 -0
  27. package/dist/app/planners/trace-plan.js +77 -0
  28. package/dist/app/source-collector.d.ts +5 -0
  29. package/dist/app/source-collector.js +33 -0
  30. package/dist/app/target-resolver.d.ts +7 -0
  31. package/dist/app/target-resolver.js +110 -0
  32. package/dist/cli/cli-input.js +70 -19
  33. package/dist/cli/flag-specs.d.ts +7 -3
  34. package/dist/cli/flag-specs.js +58 -7
  35. package/dist/cli/help-specs.d.ts +205 -0
  36. package/dist/cli/help-specs.js +136 -0
  37. package/dist/cli/help.js +107 -28
  38. package/dist/cli/report-command.js +59 -26
  39. package/dist/commands/{run-command.d.ts → git-command.d.ts} +3 -3
  40. package/dist/commands/git-command.js +7 -0
  41. package/dist/commands/grep-command.d.ts +11 -0
  42. package/dist/commands/grep-command.js +7 -0
  43. package/dist/commands/pack-command.d.ts +11 -0
  44. package/dist/commands/pack-command.js +7 -0
  45. package/dist/commands/scope-command.d.ts +12 -0
  46. package/dist/commands/scope-command.js +7 -0
  47. package/dist/commands/shared-runner.d.ts +12 -0
  48. package/dist/commands/shared-runner.js +30 -0
  49. package/dist/commands/trace-command.d.ts +11 -0
  50. package/dist/commands/trace-command.js +7 -0
  51. package/dist/config/default-config.js +7 -11
  52. package/dist/config/load.d.ts +1 -0
  53. package/dist/config/load.js +269 -4
  54. package/dist/config/migration/detect.d.ts +2 -1
  55. package/dist/config/migration/detect.js +10 -2
  56. package/dist/config/migration/messages.js +12 -11
  57. package/dist/config/migration/transform.js +77 -32
  58. package/dist/filesystem/binary.d.ts +2 -0
  59. package/dist/filesystem/binary.js +30 -0
  60. package/dist/filesystem/entry-discovery.d.ts +14 -0
  61. package/dist/filesystem/entry-discovery.js +78 -0
  62. package/dist/filesystem/file-set.d.ts +7 -0
  63. package/dist/filesystem/file-set.js +57 -0
  64. package/dist/filesystem/glob-scan.d.ts +6 -2
  65. package/dist/filesystem/glob-scan.js +12 -7
  66. package/dist/filesystem/path-patterns.d.ts +20 -0
  67. package/dist/filesystem/path-patterns.js +70 -0
  68. package/dist/index.js +51 -29
  69. package/dist/output/markdown-sections.d.ts +5 -0
  70. package/dist/output/markdown-sections.js +103 -0
  71. package/dist/output/markdown-toc.d.ts +31 -0
  72. package/dist/output/markdown-toc.js +287 -0
  73. package/dist/output/markdown.d.ts +5 -8
  74. package/dist/output/markdown.js +80 -127
  75. package/dist/output/produce-output.d.ts +4 -1
  76. package/dist/output/produce-output.js +6 -7
  77. package/dist/output/render-helpers.d.ts +4 -0
  78. package/dist/output/render-helpers.js +14 -0
  79. package/dist/output/text.d.ts +2 -2
  80. package/dist/output/text.js +47 -8
  81. package/dist/resolvers/js/resolve-alias.js +10 -41
  82. package/dist/resolvers/php/bindings.js +6 -6
  83. package/dist/resolvers/php/extract-imports.d.ts +19 -7
  84. package/dist/resolvers/php/extract-imports.js +146 -26
  85. package/dist/resolvers/php/php-resolver.js +55 -24
  86. package/dist/resolvers/php/psr4.d.ts +1 -1
  87. package/dist/resolvers/php/psr4.js +7 -1
  88. package/dist/runtime/shell-command-runner.d.ts +5 -0
  89. package/dist/runtime/shell-command-runner.js +118 -0
  90. package/dist/tracing/collect-trace.d.ts +10 -0
  91. package/dist/tracing/collect-trace.js +23 -0
  92. package/dist/tracing/exclude.d.ts +0 -5
  93. package/dist/tracing/exclude.js +16 -12
  94. package/dist/tracing/follow-chain.js +2 -2
  95. package/dist/types/app.types.d.ts +38 -8
  96. package/dist/types/artifact.types.d.ts +50 -0
  97. package/dist/types/artifact.types.js +2 -0
  98. package/dist/types/cli.types.d.ts +52 -4
  99. package/dist/types/config.types.d.ts +37 -20
  100. package/dist/types/index.d.ts +2 -0
  101. package/dist/types/index.js +2 -0
  102. package/dist/types/output.types.d.ts +12 -3
  103. package/dist/types/planner.types.d.ts +76 -0
  104. package/dist/types/planner.types.js +2 -0
  105. package/dist/types/resolver.types.d.ts +1 -1
  106. package/dist/types/tracing.types.d.ts +0 -11
  107. package/package.json +2 -2
  108. package/schema/prodex.schema.json +59 -41
  109. package/dist/app/run-plans.d.ts +0 -11
  110. package/dist/app/run-plans.js +0 -61
  111. package/dist/commands/profiles-command.d.ts +0 -6
  112. package/dist/commands/profiles-command.js +0 -16
  113. package/dist/commands/run-command.js +0 -17
  114. package/dist/config/build-config.d.ts +0 -13
  115. package/dist/config/build-config.js +0 -127
  116. package/dist/tracing/include-files.d.ts +0 -2
  117. package/dist/tracing/include-files.js +0 -36
  118. package/dist/tracing/trace-run.d.ts +0 -2
  119. package/dist/tracing/trace-run.js +0 -21
@@ -12,13 +12,12 @@ const render_constants_1 = require("./render-constants");
12
12
  const markdown_1 = require("./markdown");
13
13
  const text_1 = require("./text");
14
14
  const path_2 = require("../filesystem/path");
15
- async function produceOutput({ name, files, cfg }) {
16
- const { output: { format, versioned, dir }, } = cfg;
15
+ async function produceOutput({ name, payload, format, dir, versioned, }) {
17
16
  let outputBase = (0, path_2.sanitizeFileName)(name || "combined");
18
17
  outputBase = `${outputBase}-${render_constants_1.SUFFIX}`;
19
18
  if (versioned)
20
19
  outputBase = `${outputBase}_${(0, naming_1.shortTimestamp)()}`;
21
- const outputDir = path_1.default.isAbsolute(dir) ? dir : path_1.default.join(cfg.root, dir);
20
+ const outputDir = path_1.default.isAbsolute(dir) ? dir : path_1.default.join(payload.root, dir);
22
21
  try {
23
22
  fs_1.default.mkdirSync(outputDir, { recursive: true });
24
23
  }
@@ -26,10 +25,10 @@ async function produceOutput({ name, files, cfg }) {
26
25
  logger_1.logger.warn("Could not create output directory:", outputDir);
27
26
  }
28
27
  const outputPath = path_1.default.join(outputDir, `${outputBase}.${format}`);
29
- const sorted = [...files].sort((a, b) => a.localeCompare(b));
30
28
  const content = format === "txt"
31
- ? [(0, text_1.tocTxt)(sorted, cfg.root), ...sorted.map((file) => (0, text_1.renderTxt)(file, cfg.root))].join("")
32
- : (0, markdown_1.renderTraceMd)(sorted, cfg.root).content;
29
+ ? (0, text_1.renderTxt)(payload)
30
+ : (0, markdown_1.renderTraceMd)(payload).content;
33
31
  fs_1.default.writeFileSync(outputPath, content, "utf8");
34
- return outputPath;
32
+ const outputSizeBytes = Buffer.byteLength(content, "utf8");
33
+ return { outputPath, outputSizeBytes };
35
34
  }
@@ -0,0 +1,4 @@
1
+ import type { LayoutOrder } from "../types";
2
+ export declare function getLayoutOrder(commandKind?: string): LayoutOrder;
3
+ export declare function formatExitCode(code: number | null): string;
4
+ export declare function formatTimeout(timedOut: boolean): string;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getLayoutOrder = getLayoutOrder;
4
+ exports.formatExitCode = formatExitCode;
5
+ exports.formatTimeout = formatTimeout;
6
+ function getLayoutOrder(commandKind) {
7
+ return commandKind === "git" ? "sections-first" : "files-first";
8
+ }
9
+ function formatExitCode(code) {
10
+ return code !== null ? String(code) : "null";
11
+ }
12
+ function formatTimeout(timedOut) {
13
+ return timedOut ? "yes" : "no";
14
+ }
@@ -1,2 +1,2 @@
1
- export declare function tocTxt(files: string[], root?: string): string;
2
- export declare function renderTxt(filePath: string, root?: string): string;
1
+ import type { ArtifactPayload } from "../types";
2
+ export declare function renderTxt(payload: ArtifactPayload): string;
@@ -1,14 +1,53 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.tocTxt = tocTxt;
4
3
  exports.renderTxt = renderTxt;
5
4
  const read_file_1 = require("../filesystem/read-file");
6
- function tocTxt(files, root = process.cwd()) {
7
- const sorted = [...files].sort((a, b) => a.localeCompare(b));
8
- return ["##==== Combined Scope ====", ...sorted.map((file) => "## - " + (0, read_file_1.rel)(file, root))].join("\n") + "\n\n";
5
+ const render_helpers_1 = require("./render-helpers");
6
+ function renderTxt(payload) {
7
+ const root = payload.root;
8
+ const sorted = [...payload.files].sort((a, b) => a.path.localeCompare(b.path));
9
+ const contextLines = [];
10
+ if (payload.metadata?.commandKind === "trace" && payload.metadata.targets && payload.metadata.targets.length > 0) {
11
+ const relativeEntries = payload.metadata.entries.map(e => (0, read_file_1.rel)(e, root));
12
+ contextLines.push("##==== Trace Target Context ====", `## - Requested Target(s): ${payload.metadata.targets.join(", ")}`, `## - Resolved Starting Point(s): ${relativeEntries.join(", ") || "none"}`, `## - Traversal Depth: ${payload.metadata.depth !== undefined ? payload.metadata.depth : "none"}`, "");
13
+ }
14
+ const layoutOrder = (0, render_helpers_1.getLayoutOrder)(payload.metadata?.commandKind);
15
+ const isFileFirst = layoutOrder === "files-first";
16
+ const sectionToc = (payload.sections ?? []).map((sec) => "## - Section: " + sec.title);
17
+ const fileToc = sorted.map((file) => "## - File: " + (0, read_file_1.rel)(file.path, root));
18
+ const tocParts = isFileFirst ? [...fileToc, ...sectionToc] : [...sectionToc, ...fileToc];
19
+ const toc = ["##==== Combined Scope ====", ...contextLines, ...tocParts].join("\n") + "\n\n";
20
+ const genericSections = (payload.sections ?? [])
21
+ .map((sec) => {
22
+ return ["##==== section: " + sec.title + " ====", "##region " + sec.title, sec.content, "##endregion", ""].join("\n");
23
+ })
24
+ .join("");
25
+ const fileSections = sorted
26
+ .map((file) => {
27
+ const relativePath = (0, read_file_1.rel)(file.path, root);
28
+ const code = file.readError ? `Error reading file: ${file.readError}` : file.content;
29
+ return ["##==== path: " + relativePath + " ====", "##region " + relativePath, code, "##endregion", ""].join("\n");
30
+ })
31
+ .join("");
32
+ const cmdAttachments = renderTxtCmdResults(payload.commandOutputs, root);
33
+ const bodyParts = isFileFirst
34
+ ? [toc, fileSections, cmdAttachments, genericSections]
35
+ : [toc, genericSections, fileSections, cmdAttachments];
36
+ return bodyParts.join("");
9
37
  }
10
- function renderTxt(filePath, root = process.cwd()) {
11
- const relativePath = (0, read_file_1.rel)(filePath, root);
12
- const code = (0, read_file_1.readFileSafe)(filePath);
13
- return ["##==== path: " + relativePath + " ====", "##region " + relativePath, code, "##endregion", ""].join("\n");
38
+ function renderTxtCmdResults(cmdResults, root = process.cwd()) {
39
+ if (!cmdResults || cmdResults.length === 0)
40
+ return "";
41
+ const lines = ["\n##==== Command Attachments ===="];
42
+ for (let i = 0; i < cmdResults.length; i++) {
43
+ const res = cmdResults[i];
44
+ const statusStr = res.status;
45
+ lines.push(`## Command ${i + 1}`, `## - Command: ${res.command}`, `## - Directory: ${(0, read_file_1.rel)(res.cwd, root) || "."}`, `## - Status: ${statusStr}`, `## - Exit Code: ${(0, render_helpers_1.formatExitCode)(res.exitCode)}`, `## - Duration: ${res.durationMs}ms`, `## - Timeout State: ${res.timedOut ? "Yes" : "No"}`, `## - Output:`);
46
+ if (res.errorMessage) {
47
+ lines.push(`## - Error Message: ${res.errorMessage}`);
48
+ }
49
+ lines.push(res.combinedOutput);
50
+ }
51
+ lines.push("##==== End Command Attachments ====\n");
52
+ return lines.join("\n");
14
53
  }
@@ -5,48 +5,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.resolveAliasPath = resolveAliasPath;
7
7
  const path_1 = __importDefault(require("path"));
8
- const cache_manager_1 = require("../../cache/cache-manager");
9
- const cache_keys_1 = require("../../cache/cache-keys");
10
- const glob_scan_1 = require("../../filesystem/glob-scan");
11
- const read_file_1 = require("../../filesystem/read-file");
12
- const path_2 = require("../../filesystem/path");
13
8
  async function resolveAliasPath(specifier, root, cfg) {
14
- if (!specifier.includes("/"))
15
- return null;
16
- const [aliasName, ...rest] = specifier.split("/");
17
- const remainder = rest.join("/");
18
- const knownAliases = cfg.resolve.aliases || {};
19
- const aliasKey = aliasName.startsWith("@") ? aliasName : `@${aliasName}`;
20
- // 1️⃣ Check config-defined aliases
21
- if (knownAliases[aliasKey]) {
22
- const relPart = remainder.replace(/^\/+/, "");
23
- return path_1.default.resolve(root, knownAliases[aliasKey], relPart);
24
- }
25
- // 2️⃣ Check cached aliases
26
- const cached = cache_manager_1.CacheManager.get(cache_keys_1.CACHE_KEYS.ALIASES, aliasKey);
27
- if (cached) {
28
- const relPart = remainder.replace(/^\/+/, "");
29
- return path_1.default.resolve(root, cached, relPart);
30
- }
31
- // 3️⃣ Fallback discovery with Fast-Glob
32
- const stripped = remainder;
33
- const hasExt = /\.[a-z0-9]+$/i.test(stripped);
34
- const patterns = hasExt ? [`**/${stripped}`] : [`**/${stripped}.*`, `**/${stripped}/index.*`];
35
- const { files: matches } = await (0, glob_scan_1.globScan)(patterns, { cwd: root });
36
- if (matches.length) {
37
- const resolvedMatch = resolveMatches(matches, remainder);
38
- if (!resolvedMatch)
39
- return null;
40
- const relPath = (0, read_file_1.rel)(resolvedMatch, cfg.root);
41
- cache_manager_1.CacheManager.set(cache_keys_1.CACHE_KEYS.ALIASES, aliasKey, relPath);
42
- return relPath;
9
+ const knownAliases = cfg.aliases || {};
10
+ const aliasKeys = Object.keys(knownAliases).sort((a, b) => b.length - a.length);
11
+ for (const aliasKey of aliasKeys) {
12
+ if (specifier === aliasKey) {
13
+ return path_1.default.resolve(root, knownAliases[aliasKey]);
14
+ }
15
+ if (specifier.startsWith(aliasKey + "/")) {
16
+ const remainder = specifier.slice(aliasKey.length + 1);
17
+ return path_1.default.resolve(root, knownAliases[aliasKey], remainder);
18
+ }
43
19
  }
44
20
  return null;
45
21
  }
46
- function resolveMatches(matches, remainder) {
47
- const foundFile = (0, path_2.normalizePath)(matches[0]);
48
- const dSplit = foundFile.split(remainder);
49
- if (dSplit.length < 2)
50
- return "";
51
- return (0, path_2.normalizePath)(dSplit[0]);
52
- }
@@ -41,14 +41,14 @@ function loadLaravelBindings(root) {
41
41
  const short = fqcn.split("\\").pop();
42
42
  importMap[short] = fqcn;
43
43
  }
44
- // 2️⃣ Extract bindings (short class names only)
45
- const bindRe = /\$this->app->(?:bind|singleton)\s*\(\s*([A-Za-z0-9_]+)::class\s*,\s*([A-Za-z0-9_]+)::class/g;
44
+ // 2️⃣ Extract bindings
45
+ const bindRe = /\$this->app->(?:bind|singleton)\s*\(\s*\\?([A-Za-z0-9_\\]+)::class\s*,\s*\\?([A-Za-z0-9_\\]+)::class/g;
46
46
  let m;
47
47
  while ((m = bindRe.exec(code))) {
48
- const ifaceShort = m[1];
49
- const implShort = m[2];
50
- const ifaceFull = importMap[ifaceShort] || ifaceShort;
51
- const implFull = importMap[implShort] || implShort;
48
+ const ifaceRaw = m[1].replace(/^\\+/, "");
49
+ const implRaw = m[2].replace(/^\\+/, "");
50
+ const ifaceFull = importMap[ifaceRaw] || ifaceRaw;
51
+ const implFull = importMap[implRaw] || implRaw;
52
52
  logger_1.logger.debug(`[laravel-bindings] ${file} => ${ifaceFull} => ${implFull}`);
53
53
  bindings[ifaceFull] = implFull;
54
54
  }
@@ -1,13 +1,25 @@
1
+ import type { PhpResolverCtx } from "../../types";
1
2
  /**
2
- * Extracts import-like references from PHP code.
3
- * Supports:
4
- * - require/include/require_once/include_once
5
- * - use statements (including grouped imports like `use App\Models\{User, Team};`)
3
+ * Extracts PHP use statements and builds an alias/ShortName to FQCN mapping.
4
+ * Handles:
5
+ * - use Foo\Bar; => { Bar: "Foo\\Bar" }
6
+ * - use Foo\Bar as Baz; => { Baz: "Foo\\Bar" }
7
+ * - use App\Models\{User, Team as Group}; => { User: "App\\Models\\User", Group: "App\\Models\\Team" }
6
8
  */
7
- export declare function extractPhpImports(code: string): Set<string>;
9
+ export declare function extractPhpUseMap(code: string): Record<string, string>;
10
+ /**
11
+ * Extracts class references, type hints, static calls, and require/include paths.
12
+ */
13
+ export declare function extractPhpReferences(code: string): Set<string>;
8
14
  /**
9
15
  * Expands grouped `use` imports into individual fully qualified names.
10
- * Example:
11
- * "App\\Models\\{User, Team}" to ["App\\Models\\User", "App\\Models\\Team"]
12
16
  */
13
17
  export declare function expandGroupedUses(raw: Set<string>): Set<string>;
18
+ /**
19
+ * Resolves a raw PHP reference to a fully qualified class name or path.
20
+ */
21
+ export declare function resolvePhpReference(reference: string, currentNamespace: string | null, useMap: Record<string, string>, ctx: PhpResolverCtx): string | null;
22
+ /**
23
+ * Backward-compatible wrapper for extracting raw PHP imports.
24
+ */
25
+ export declare function extractPhpImports(code: string): Set<string>;
@@ -1,47 +1,122 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.extractPhpImports = extractPhpImports;
3
+ exports.extractPhpUseMap = extractPhpUseMap;
4
+ exports.extractPhpReferences = extractPhpReferences;
4
5
  exports.expandGroupedUses = expandGroupedUses;
6
+ exports.resolvePhpReference = resolvePhpReference;
7
+ exports.extractPhpImports = extractPhpImports;
5
8
  /**
6
- * Extracts import-like references from PHP code.
7
- * Supports:
8
- * - require/include/require_once/include_once
9
- * - use statements (including grouped imports like `use App\Models\{User, Team};`)
9
+ * Extracts PHP use statements and builds an alias/ShortName to FQCN mapping.
10
+ * Handles:
11
+ * - use Foo\Bar; => { Bar: "Foo\\Bar" }
12
+ * - use Foo\Bar as Baz; => { Baz: "Foo\\Bar" }
13
+ * - use App\Models\{User, Team as Group}; => { User: "App\\Models\\User", Group: "App\\Models\\Team" }
10
14
  */
11
- function extractPhpImports(code) {
15
+ function extractPhpUseMap(code) {
16
+ const useMap = {};
17
+ // Match statements like: use App\Models\User; or use App\Models\{User, Team as Group};
18
+ // Skip "use function" or "use const"
19
+ const useStmtRe = /\buse\s+(?!(?:function|const)\s+)([^;]+);/g;
20
+ let match;
21
+ while ((match = useStmtRe.exec(code))) {
22
+ const block = match[1].trim();
23
+ // Check if it's a grouped use statement
24
+ const groupMatch = block.match(/^(.+?)\s*{([^}]+)}/);
25
+ if (groupMatch) {
26
+ const base = groupMatch[1].trim().replace(/\\+$/, "");
27
+ const items = groupMatch[2].split(",").map((i) => i.trim()).filter(Boolean);
28
+ for (const item of items) {
29
+ const asMatch = item.match(/^(.+?)\s+as\s+(.+)$/i);
30
+ if (asMatch) {
31
+ const fqcn = `${base}\\${asMatch[1].trim()}`;
32
+ const alias = asMatch[2].trim();
33
+ useMap[alias] = fqcn;
34
+ }
35
+ else {
36
+ const fqcn = `${base}\\${item}`;
37
+ const shortName = item.split("\\").pop();
38
+ useMap[shortName] = fqcn;
39
+ }
40
+ }
41
+ }
42
+ else {
43
+ // Single or comma-separated use statements (e.g. "Foo\Bar, Foo\Baz")
44
+ const parts = block.split(",").map((p) => p.trim()).filter(Boolean);
45
+ for (const part of parts) {
46
+ const asMatch = part.match(/^(.+?)\s+as\s+(.+)$/i);
47
+ if (asMatch) {
48
+ const fqcn = asMatch[1].trim();
49
+ const alias = asMatch[2].trim();
50
+ useMap[alias] = fqcn;
51
+ }
52
+ else {
53
+ const fqcn = part;
54
+ const shortName = part.split("\\").pop();
55
+ useMap[shortName] = fqcn;
56
+ }
57
+ }
58
+ }
59
+ }
60
+ return useMap;
61
+ }
62
+ /**
63
+ * Extracts class references, type hints, static calls, and require/include paths.
64
+ */
65
+ function extractPhpReferences(code) {
12
66
  const out = new Set();
13
- const patterns = [/\b(?:require|include|require_once|include_once)\s*\(?['"]([^'"]+)['"]\)?/g, /\buse\s+([A-Z][\w\\]+(?:\s*{[^}]+})?)/g];
14
- for (const r of patterns) {
15
- let m;
67
+ // require/include patterns
68
+ const requireRe = /\b(?:require|include|require_once|include_once)\s*\(?['"]([^'"]+)['"]\)?/g;
69
+ let m;
70
+ while ((m = requireRe.exec(code))) {
71
+ const val = m[1];
72
+ if (val)
73
+ out.add(val);
74
+ }
75
+ // Detect class references (e.g. new User(), User::class, User::method())
76
+ const classPatterns = [
77
+ /\bnew\s+([A-Za-z0-9_\\]+)\b/g,
78
+ /\b([A-Za-z0-9_\\]+)::class\b/g,
79
+ /\b([A-Za-z0-9_\\]+)::[A-Za-z_]/g,
80
+ /:\s*([A-Za-z0-9_\\]+)\b/g
81
+ ];
82
+ for (const r of classPatterns) {
16
83
  while ((m = r.exec(code))) {
17
- const val = m[1];
18
- if (val)
19
- out.add(val);
84
+ const className = m[1].trim();
85
+ if (/^[0-9]+$/.test(className) || ["self", "parent", "static"].includes(className.toLowerCase())) {
86
+ continue;
87
+ }
88
+ out.add(className);
20
89
  }
21
90
  }
22
- // Detect short class names
23
- const shortClassPatterns = [/\bnew\s+([A-Z][A-Za-z0-9_]*)\b/g, /\b([A-Z][A-Za-z0-9_]*)::class\b/g, /\b([A-Z][A-Za-z0-9_]*)::[A-Za-z_]/g, /:\s*([A-Z][A-Za-z0-9_]*)\b/g];
24
- // Direct matches
25
- for (const r of shortClassPatterns) {
26
- let m;
27
- while ((m = r.exec(code)))
28
- out.add(m[1]);
29
- }
30
91
  // Type-hinted parameters in function signatures
31
- let m;
32
92
  const paramPattern = /\(([^)]*)\)/g;
33
93
  while ((m = paramPattern.exec(code))) {
34
94
  const block = m[1];
35
- const types = block.match(/\b([A-Z][A-Za-z0-9_]*)\b/g);
36
- if (types)
37
- types.forEach((t) => out.add(t));
95
+ const params = block.split(",").map((p) => p.trim()).filter(Boolean);
96
+ for (const param of params) {
97
+ const parts = param.split("$");
98
+ if (parts.length > 1) {
99
+ const typePart = parts[0].trim();
100
+ if (typePart) {
101
+ const types = typePart.match(/\b([A-Za-z0-9_\\]+)\b/g);
102
+ if (types) {
103
+ for (const t of types) {
104
+ const trimmedT = t.trim();
105
+ if (trimmedT &&
106
+ !/^[0-9]+$/.test(trimmedT) &&
107
+ !["self", "parent", "static", "array", "callable", "string", "int", "bool", "float", "iterable", "object"].includes(trimmedT.toLowerCase())) {
108
+ out.add(trimmedT);
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
38
115
  }
39
116
  return out;
40
117
  }
41
118
  /**
42
119
  * Expands grouped `use` imports into individual fully qualified names.
43
- * Example:
44
- * "App\\Models\\{User, Team}" to ["App\\Models\\User", "App\\Models\\Team"]
45
120
  */
46
121
  function expandGroupedUses(raw) {
47
122
  const out = new Set();
@@ -61,3 +136,48 @@ function expandGroupedUses(raw) {
61
136
  }
62
137
  return out;
63
138
  }
139
+ /**
140
+ * Resolves a raw PHP reference to a fully qualified class name or path.
141
+ */
142
+ function resolvePhpReference(reference, currentNamespace, useMap, ctx) {
143
+ if (!reference || typeof reference !== "string")
144
+ return null;
145
+ if (reference.startsWith(".") || reference.includes("/") || reference.endsWith(".php")) {
146
+ return reference;
147
+ }
148
+ const segments = reference.split("\\");
149
+ const firstSegment = segments[0];
150
+ if (useMap[firstSegment]) {
151
+ const remainder = segments.slice(1).join("\\");
152
+ let resolved = remainder ? `${useMap[firstSegment]}\\${remainder}` : useMap[firstSegment];
153
+ resolved = resolved.replace(/^\\+/, "");
154
+ if (ctx.bindings[resolved])
155
+ resolved = ctx.bindings[resolved];
156
+ return resolved;
157
+ }
158
+ let resolved = reference;
159
+ const isFullyQualified = resolved.includes("\\") || resolved.startsWith("\\");
160
+ if (!isFullyQualified && currentNamespace) {
161
+ resolved = `${currentNamespace}\\${resolved}`;
162
+ }
163
+ resolved = resolved.replace(/^\\+/, "");
164
+ if (ctx.bindings[resolved]) {
165
+ resolved = ctx.bindings[resolved];
166
+ }
167
+ return resolved;
168
+ }
169
+ /**
170
+ * Backward-compatible wrapper for extracting raw PHP imports.
171
+ */
172
+ function extractPhpImports(code) {
173
+ const out = new Set();
174
+ const useMap = extractPhpUseMap(code);
175
+ for (const fqcn of Object.values(useMap)) {
176
+ out.add(fqcn);
177
+ }
178
+ const refs = extractPhpReferences(code);
179
+ for (const ref of refs) {
180
+ out.add(ref);
181
+ }
182
+ return out;
183
+ }
@@ -24,13 +24,26 @@ async function resolvePhpImports({ cfg, filePath, ctx }) {
24
24
  return (0, resolver_result_1.emptyResolverResult)();
25
25
  const currentNamespace = getCurrentNamespace(code);
26
26
  const phpCtx = buildPhpCtx(cfg.root, ctx);
27
- const imports = (0, extract_imports_1.expandGroupedUses)((0, extract_imports_1.extractPhpImports)(code));
27
+ const useMap = (0, extract_imports_1.extractPhpUseMap)(code);
28
+ const rawReferences = (0, extract_imports_1.extractPhpReferences)(code);
29
+ const candidateImports = new Set();
30
+ for (const fqcn of Object.values(useMap)) {
31
+ candidateImports.add(fqcn);
32
+ }
33
+ for (const ref of rawReferences) {
34
+ const resolvedRef = (0, extract_imports_1.resolvePhpReference)(ref, currentNamespace, useMap, phpCtx);
35
+ if (resolvedRef) {
36
+ candidateImports.add(resolvedRef);
37
+ }
38
+ }
28
39
  const stats = (0, resolver_result_1.newResolverStats)();
29
40
  const files = [];
30
- for (const importName of imports) {
31
- const resolvedImport = resolveNamespaceImport(importName, currentNamespace, phpCtx);
32
- if (!resolvedImport)
41
+ for (const resolvedImport of candidateImports) {
42
+ const isFilePath = resolvedImport.startsWith(".") || resolvedImport.includes("/") || resolvedImport.endsWith(".php");
43
+ const matchesNamespace = startsWithAnyNamespace(resolvedImport, phpCtx.nsKeys);
44
+ if (!isFilePath && !matchesNamespace) {
33
45
  continue;
46
+ }
34
47
  const resolvedPath = await tryResolvePhpFile(resolvedImport, filePath, phpCtx.psr4);
35
48
  stats.expected.add(resolvedImport);
36
49
  if (!resolvedPath)
@@ -48,33 +61,50 @@ function getCurrentNamespace(code) {
48
61
  const nsMatch = code.match(/\bnamespace\s+([^;]+);/);
49
62
  return nsMatch ? nsMatch[1].trim() : null;
50
63
  }
51
- function resolveNamespaceImport(importName, currentNamespace, ctx) {
52
- if (!importName || typeof importName !== "string")
53
- return null;
54
- let resolved = importName;
55
- const isFullyQualified = resolved.includes("\\") || resolved.startsWith("\\");
56
- if (!isFullyQualified && currentNamespace)
57
- resolved = `${currentNamespace}\\${resolved}`;
58
- if (ctx.bindings[resolved])
59
- resolved = ctx.bindings[resolved];
60
- return startsWithAnyNamespace(resolved, ctx.nsKeys) ? resolved : null;
61
- }
62
64
  async function tryResolvePhpFile(imp, fromFile, psr4) {
63
65
  const key = `php:${imp}:${fromFile}`;
64
66
  const cached = cache_manager_1.CacheManager.get(cache_keys_1.CACHE_KEYS.PHP_FILECACHE, key);
65
67
  if (cached !== undefined)
66
68
  return cached;
67
- const nsKey = Object.keys(psr4).find((candidate) => imp.startsWith(candidate));
69
+ if (imp.startsWith(".") || imp.includes("/") || imp.endsWith(".php")) {
70
+ const absolutePath = path_1.default.resolve(path_1.default.dirname(fromFile), imp);
71
+ try {
72
+ const stats = await promises_1.default.stat(absolutePath);
73
+ if (stats.isFile()) {
74
+ const resolved = path_1.default.resolve(absolutePath);
75
+ cache_manager_1.CacheManager.set(cache_keys_1.CACHE_KEYS.PHP_FILECACHE, key, resolved);
76
+ return resolved;
77
+ }
78
+ }
79
+ catch { }
80
+ if (!imp.endsWith(".php")) {
81
+ try {
82
+ const stats = await promises_1.default.stat(absolutePath + ".php");
83
+ if (stats.isFile()) {
84
+ const resolved = path_1.default.resolve(absolutePath + ".php");
85
+ cache_manager_1.CacheManager.set(cache_keys_1.CACHE_KEYS.PHP_FILECACHE, key, resolved);
86
+ return resolved;
87
+ }
88
+ }
89
+ catch { }
90
+ }
91
+ cache_manager_1.CacheManager.set(cache_keys_1.CACHE_KEYS.PHP_FILECACHE, key, null);
92
+ return null;
93
+ }
94
+ const nsKey = Object.keys(psr4).find((candidate) => {
95
+ return imp === candidate || imp.startsWith(candidate + "\\");
96
+ });
68
97
  if (!nsKey) {
69
98
  cache_manager_1.CacheManager.set(cache_keys_1.CACHE_KEYS.PHP_FILECACHE, key, null);
70
99
  return null;
71
100
  }
72
- const relativeImport = (0, path_2.normalizePath)(imp.replace(nsKey, ""));
73
- const candidates = [
74
- path_1.default.join(psr4[nsKey], relativeImport),
75
- path_1.default.join(psr4[nsKey], relativeImport + ".php"),
76
- path_1.default.join(psr4[nsKey], relativeImport, "index.php"),
77
- ];
101
+ const relativeImport = (0, path_2.normalizePath)(imp.slice(nsKey.length).replace(/^\\+/, ""));
102
+ const mappedDirs = psr4[nsKey];
103
+ const dirs = Array.isArray(mappedDirs) ? mappedDirs : [mappedDirs];
104
+ const candidates = [];
105
+ for (const dir of dirs) {
106
+ candidates.push(path_1.default.join(dir, relativeImport), path_1.default.join(dir, relativeImport + ".php"), path_1.default.join(dir, relativeImport, "index.php"));
107
+ }
78
108
  const results = await Promise.allSettled(candidates.map(async (candidate) => {
79
109
  try {
80
110
  const stats = await promises_1.default.stat(candidate);
@@ -97,9 +127,10 @@ function buildPhpCtx(root, prev) {
97
127
  return { kind: "php", psr4, nsKeys, bindings };
98
128
  }
99
129
  function startsWithAnyNamespace(imp, nsKeys) {
100
- for (const nsKey of nsKeys)
101
- if (imp.startsWith(nsKey))
130
+ for (const nsKey of nsKeys) {
131
+ if (imp === nsKey || imp.startsWith(nsKey + "\\"))
102
132
  return true;
133
+ }
103
134
  return false;
104
135
  }
105
136
  function isFulfilledPath(result) {
@@ -2,4 +2,4 @@
2
2
  * Builds a PSR-4 namespace to directory map from composer.json.
3
3
  * Returns absolute paths in the map values.
4
4
  */
5
- export declare function resolvePsr4(root: string): Record<string, string>;
5
+ export declare function resolvePsr4(root: string): Record<string, string | string[]>;
@@ -27,7 +27,13 @@ function resolvePsr4(root) {
27
27
  const map = {};
28
28
  for (const ns in src) {
29
29
  const cleanNs = ns.replace(/\\+$/, "");
30
- map[cleanNs] = path_1.default.resolve(root, src[ns]);
30
+ const val = src[ns];
31
+ if (Array.isArray(val)) {
32
+ map[cleanNs] = val.map((v) => path_1.default.resolve(root, v));
33
+ }
34
+ else {
35
+ map[cleanNs] = path_1.default.resolve(root, val);
36
+ }
31
37
  }
32
38
  cache_manager_1.CacheManager.set(cache_keys_1.CACHE_KEYS.PHP_PSR4, root, map);
33
39
  return map;
@@ -0,0 +1,5 @@
1
+ import type { CommandOutputResult } from "../types";
2
+ /**
3
+ * Execute a single command sequentially using child_process.spawn.
4
+ */
5
+ export declare function executeAttachedCommand(command: string, cwd: string, timeoutSeconds: number): Promise<CommandOutputResult>;