# 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);
    }