File size: 2,462 Bytes
71d239c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7603aa2
71d239c
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
"""The static-analysis engine: ingested files -> a factual ProjectModel.

This is StoryCode's source of truth. It dispatches each file to the right parser
(Python `ast`, JS/TS tree-sitter, or the lightweight generic pass), reads the
dependency manifests, then builds the project graph (edges, fan-in/out, roles,
entry points, safe-to-edit). The language model is never involved here.
"""
from __future__ import annotations

import posixpath

import config
from ingest import Ingested
from schema import FileInfo, ProjectModel

from analyzer import deps, generic, graph, js_treesitter, python_ast


def _loc(text: str) -> int:
    return sum(1 for line in text.splitlines() if line.strip())


def _parse_one(path: str, lang: str, text: str):
    if lang == "python":
        return python_ast.parse(text)
    if lang in ("javascript", "typescript"):
        return js_treesitter.parse(text, lang)
    if lang in ("html", "css"):
        return generic.parse(text, lang)
    return [], []


def analyze_project(ingested: Ingested) -> ProjectModel:
    """Turn ingested source files into a fully-populated ProjectModel."""
    files: list[FileInfo] = []
    dependencies = []
    langs: set[str] = set()

    for sf in ingested.files:
        base = posixpath.basename(sf.path).lower()
        if base in config.DEP_MANIFESTS:
            dependencies.extend(deps.parse_manifest(base, sf.text))
            # package.json / pyproject also count as config files in the graph.
        symbols, imports = _parse_one(sf.path, sf.lang, sf.text)
        if sf.lang in config.LANG_BY_EXT.values():
            langs.add(sf.lang)
        files.append(FileInfo(
            path=sf.path, lang=sf.lang, loc=_loc(sf.text),
            symbols=symbols, imports=imports,
            role=graph.classify_role(sf.path, sf.lang),
        ))

    graph.resolve_edges(files)
    graph.compute_metrics(files)

    # De-dup dependencies by (name, manifest), keep order.
    seen: set[tuple[str, str]] = set()
    uniq_deps = []
    for d in dependencies:
        key = (d.name.lower(), d.manifest)
        if key not in seen:
            seen.add(key)
            uniq_deps.append(d)

    return ProjectModel(
        name=ingested.name,
        files=files,
        deps=uniq_deps,
        entry_points=graph.detect_entry_points(files),
        languages=sorted(langs),
        skipped=ingested.skipped,
        secrets_found=ingested.secrets_found,
        note=ingested.note,
    )