All files ast-utils.ts

26.92% Statements 7/26
65.21% Branches 15/23
42.85% Functions 3/7
30% Lines 6/20

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                          34x 34x   34x           52x                                             5x 5x                                                                                                          
import { parseFileExports } from '@aiready/core';
import type { ExportInfo, DependencyNode, FileClassification } from './types';
import { inferDomain, extractExports } from './semantic-analysis';
 
/**
 * Extract exports using high-fidelity AST parsing across 5+ languages
 */
export function extractExportsWithAST(
  content: string,
  filePath: string,
  domainOptions?: { domainKeywords?: string[] },
  fileImports?: string[]
): ExportInfo[] {
  try {
    const { exports: astExports } = 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 (error) {
    // Ultimate fallback
    return extractExports(content, filePath, domainOptions, fileImports);
  }
}
 
/**
 * Check if a file is a test, mock, or fixture file
 */
export function isTestFile(filePath: string): boolean {
  const lower = filePath.toLowerCase();
  return (
    lower.includes('.test.') ||
    lower.includes('.spec.') ||
    lower.includes('/__tests__/') ||
    lower.includes('/tests/') ||
    lower.includes('/test/') ||
    lower.includes('test-') ||
    lower.includes('-test') ||
    lower.includes('/__mocks__/') ||
    lower.includes('/mocks/') ||
    lower.includes('/fixtures/') ||
    lower.includes('.mock.') ||
    lower.includes('.fixture.') ||
    lower.includes('/test-utils/')
  );
}
 
/**
 * 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);
}