All files discover.ts

100% Statements 62/62
95.45% Branches 21/22
100% Functions 5/5
100% Lines 62/62

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 1271x                                                           1x 1x 1x 1x 1x   1x 53x 53x 53x 53x 53x   53x 53x 53x   53x 53x 53x 53x 53x 53x 53x   53x 38x 15x 53x 53x                         53x 53x 53x 53x 53x 2x 2x 2x   51x 53x 50x 53x 38x 38x                       53x 53x 53x 53x 53x 53x 53x 53x 53x 53x 53x 53x 57x               57x 57x 57x 57x 57x 53x 53x 53x  
/**
 * Go file discovery.
 *
 * Strategy mirrors graph-rust:
 *   1. Locate `go.mod`. We do NOT parse it — recursive `.go` glob with
 *      vendor/ excluded handles single modules and most workspace
 *      layouts. Go workspace files (`go.work`) listing multiple
 *      modules would require parsing; that's a follow-up.
 *   2. If no go.mod present, configPath is undefined; cacheKey falls
 *      back to the literal `no-config`.
 *   3. Records go.sum (if present) as the fingerprint — prefers go.sum
 *      since it holds the resolved-dep hashes; falls back to go.mod.
 *
 * Excluded directories:
 *   - `vendor/` — Go's vendored-dep directory; semantically third-party.
 *   - `node_modules/` — rare in Go projects but defensive.
 *   - `.git/` — VCS metadata.
 *
 * Returns absolute, realpath-normalized, sorted, deduped paths so I-9
 * (referential transparency of discoverFiles) holds across runs.
 */
 
import { existsSync, realpathSync } from 'node:fs';
import { resolve, sep } from 'node:path';
 
import { logger } from '@opensip-tools/core';
import { glob } from 'glob';
 
import type { DiscoverInput, DiscoverOutput } from '@opensip-tools/graph';
 
const EXCLUDED_DIR_GLOBS: readonly string[] = [
  '**/vendor/**',
  '**/node_modules/**',
  '**/.git/**',
];
 
export function discoverFiles(input: DiscoverInput): DiscoverOutput {
  logger.info({
    evt: 'graph.discover.start',
    module: 'graph:discover:go',
    projectDir: input.cwd,
  });
 
  const projectDirAbs = normalizeProjectDir(input.cwd);
  const configPathAbs = resolveConfigPath(projectDirAbs, input.configPathOverride);
  const files = collectGoFiles(projectDirAbs);
 
  logger.info({
    evt: 'graph.discover.complete',
    module: 'graph:discover:go',
    projectDir: projectDirAbs,
    configPath: configPathAbs ?? '(none)',
    fileCount: files.length,
  });
 
  const out: DiscoverOutput = configPathAbs === undefined
    ? { projectDirAbs, files }
    : { projectDirAbs, files, configPathAbs };
  return out;
}
 
/* v8 ignore start */
function normalizeProjectDir(projectDir: string): string {
  const abs = resolve(projectDir);
  try {
    return realpathSync(abs);
  } catch {
    return abs;
  }
}
/* v8 ignore stop */
 
function resolveConfigPath(
  projectDirAbs: string,
  override: string | undefined,
): string | undefined {
  if (override !== undefined && override.length > 0) {
    const abs = resolve(projectDirAbs, override);
    return existsSync(abs) ? realpathOrPath(abs) : abs;
  }
  // Prefer go.sum (resolved deps with hashes) over go.mod (intent).
  const sum = resolve(projectDirAbs, 'go.sum');
  if (existsSync(sum)) return realpathOrPath(sum);
  const mod = resolve(projectDirAbs, 'go.mod');
  if (existsSync(mod)) return realpathOrPath(mod);
  return undefined;
}
 
/* v8 ignore start */
function realpathOrPath(p: string): string {
  try {
    return realpathSync(p);
  } catch {
    return p;
  }
}
/* v8 ignore stop */
 
function collectGoFiles(projectDirAbs: string): readonly string[] {
  const matches: string[] = glob.sync('**/*.go', {
    cwd: projectDirAbs,
    absolute: true,
    ignore: [...EXCLUDED_DIR_GLOBS],
    nodir: true,
    follow: false,
    dot: false,
  });
  const seen = new Set<string>();
  const out: string[] = [];
  for (const m of matches) {
    let real: string = m;
    /* v8 ignore start */
    try {
      real = realpathSync(m);
    } catch {
      // fall through with original
    }
    /* v8 ignore stop */
    const key = real.split(sep).join('/');
    if (seen.has(key)) continue;
    seen.add(key);
    out.push(real);
  }
  out.sort();
  return out;
}