All files / src ast-utils.ts

20.83% Statements 5/24
20% Branches 2/10
33.33% Functions 2/6
22.22% Lines 4/18

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                                        39x 39x   39x           57x                                                                                                              
import { parseFileExports, isTestFile } from '@aiready/core';
import type { ExportInfo } from './types';
import { inferDomain, extractExports } from './semantic/domain-inference';
 
/**
 * Extract exports using high-fidelity AST parsing across 5+ languages.
 *
 * @param content - File contents to parse.
 * @param filePath - Path to the file for language detection and context.
 * @param domainOptions - Optional configuration for domain detection.
 * @param fileImports - Optional array of strings for resolving imports.
 * @returns Array of high-fidelity export metadata.
 * @lastUpdated 2026-03-18
 */
export async function extractExportsWithAST(
  content: string,
  filePath: string,
  domainOptions?: { domainKeywords?: string[] },
  fileImports?: string[]
): Promise<ExportInfo[]> {
  try {
    const { exports: astExports } = await parseFileExports(content, filePath);
 
    Iif (astExports.length === 0 && !isTestFile(filePath)) {
      // If AST fails to find anything, we still use regex as a last resort
      // ONLY for unknown file types or very complex macros
      return extractExports(content, filePath, domainOptions, fileImports);
    }
 
    return astExports.map((exp) => ({
      name: exp.name,
      type: exp.type as any,
      inferredDomain: inferDomain(
        exp.name,
        filePath,
        domainOptions,
        fileImports
      ),
      imports: exp.imports,
      dependencies: exp.dependencies,
      typeReferences: (exp as any).typeReferences,
    }));
  } catch {
    // Ultimate fallback
    return extractExports(content, filePath, domainOptions, fileImports);
  }
}
 
/**
 * Heuristic to check if all exports share a common entity noun
 */
export function allExportsShareEntityNoun(exports: ExportInfo[]): boolean {
  if (exports.length < 2) return true;
 
  const getEntityNoun = (name: string): string | null => {
    // Basic heuristic: last part of camelCase name often is the entity
    // e.g. createOrder -> order, getUserProfile -> profile
    // But we also look for common domain nouns in the middle
    const commonNouns = [
      'user',
      'order',
      'product',
      'session',
      'account',
      'receipt',
      'token',
    ];
    const lower = name.toLowerCase();
 
    for (const noun of commonNouns) {
      if (lower.includes(noun)) return noun;
    }
 
    // Fallback: split by capital letters and take the last part
    const parts = name.split(/(?=[A-Z])/);
    return parts[parts.length - 1].toLowerCase();
  };
 
  const nouns = exports.map((e) => getEntityNoun(e.name)).filter(Boolean);
  if (nouns.length < exports.length * 0.7) return false;
 
  const firstNoun = nouns[0];
  return nouns.every((n) => n === firstNoun);
}