Skip to content

Commit a316478

Browse files
authored
Lang adding php support (#55)
* Adding initial PHP support based on Wordpress * Getting PHP support on par with other languages and adding frameworks support * Updating the pipeline image
1 parent e3ca08c commit a316478

52 files changed

Lines changed: 4425 additions & 23 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ARCHITECTURE.md

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -107,26 +107,49 @@ A snapshot is produced by a fixed, deterministic pipeline ([`internal/engine/eng
107107
Repository
108108
109109
110-
File Walker ──▶ Extractors ──▶ Fact Store ──▶ Cross-Repo Linker ──▶ Graph Index
111-
(apply (Go, Java, (indexed by (only with 2+ (bidirectional)
112-
ignore Kotlin, Python, kind / file / repos loaded) │
113-
globs) TS, Swift, name / repo) ▼
114-
Ruby, C++, OpenAPI) Explainers
115-
(cycles, layers,
116-
crossrepo)
117-
118-
119-
Renderer
120-
(llm_context.md)
121-
122-
123-
Artifacts
124-
(.enola/)
110+
┌──────────────────┐ apply ignore globs (a few extractors also scan
111+
│ File Walker │ config-format files directly — see Supported languages)
112+
└──────────────────┘
113+
114+
115+
┌──────────────────┐ Go · Java · Kotlin · JS/TS · Vue · Svelte · Python ·
116+
│ Extractors │ Swift · Ruby · C++ · PHP · OpenAPI (source → facts)
117+
└──────────────────┘
118+
119+
120+
┌──────────────────┐
121+
│ Fact Store │ indexed by kind / file / name / repo
122+
└──────────────────┘
123+
124+
125+
┌──────────────────┐
126+
│ Cross-Repo Linker│ only with 2+ repos loaded
127+
└──────────────────┘
128+
129+
130+
┌──────────────────┐
131+
│ Graph Index │ bidirectional (+ synthetic edges)
132+
└──────────────────┘
133+
134+
135+
┌──────────────────┐ cycles · layers · crossrepo · coverage · unused-routes ·
136+
│ Explainers │ god-class · hotspots · dependency-depth ·
137+
└──────────────────┘ exported-surface · complexity-outliers (facts → insights)
138+
139+
140+
┌──────────────────┐
141+
│ Renderer │ llm_context.md (snapshot → artifacts)
142+
└──────────────────┘
143+
144+
145+
┌──────────────────┐
146+
│ Artifacts │ .enola/
147+
└──────────────────┘
125148
```
126149

127150
Stage by stage:
128151

129-
1. **File walker** — enumerates files under the repo, applying the `ignore` globs from config so build output, vendored code, tests, and generated files never reach the parsers.
152+
1. **File walker** — enumerates files under the repo, applying the `ignore` globs from config so build output, vendored code, tests, and generated files never reach the parsers. A few extractors (OpenAPI, and PHP's Symfony route config) deliberately scan specific config-format files (YAML/JSON) directly from disk, bypassing these globs — because the globs exist to suppress config/data noise, not to hide those architecturally meaningful files (see [Supported languages](#supported-languages)).
130153
2. **Extractors** — each enabled language extractor first *detects* whether it applies (e.g. Go runs when there's a `go.mod`), then *parses* the matching files and emits facts. This is pure parsing; see [Supported languages](#supported-languages) for what each one understands.
131154
3. **Fact store** — facts land in an in-memory store ([`internal/facts/store.go`](internal/facts/store.go)) indexed by kind, file, name, and repo for fast queries. In append mode, facts are tagged with a repo label and file paths are repo-prefixed.
132155
4. **Cross-repo linker** — only when two or more repos are loaded. It connects the per-repo graphs by matching HTTP client/server routes and shared-library imports, emitting `service` nodes and cross-repo dependency edges. The link set is recomputed from scratch on every append, so it always reflects exactly the repos currently loaded.
@@ -448,6 +471,7 @@ Each extractor is detected by characteristic project files and then parses what
448471
| Swift | tree-sitter | `Package.swift`, `.xcodeproj`, or `.xcworkspace` present |
449472
| Ruby | tree-sitter | `Gemfile` present |
450473
| C++ | tree-sitter | a C++ source (`.cpp`/`.cc`/`.cxx`/`.hpp`/...) present, or a build file (`CMakeLists.txt`/`Makefile`/`meson.build`/`*.vcxproj`) plus any header |
474+
| PHP | tree-sitter | `composer.json`, a WordPress bootstrap file (`wp-load.php`/`wp-settings.php`/`wp-config.php`), or any `.php` source within 3 directory levels |
451475
| OpenAPI | YAML/JSON scanner| any file containing `openapi:` or `swagger:` |
452476

453477
**Go** uses the standard-library parser directly, so symbols, methods, interfaces, imports, and call edges are exact.
@@ -474,6 +498,8 @@ Each extractor is detected by characteristic project files and then parses what
474498

475499
**C++** is parsed with tree-sitter and handles the header/source split that defines the language. Classes, structs, unions, enums (incl. `enum class`), namespaces, free functions and methods, data members, and `typedef`/`using` aliases become symbol facts named `<dir>.<ns1::ns2::Class::member>` — enola's `<dir>.` module convention on the outside, native C++ `::` scope inside. Because an out-of-line definition `Class::method` (parsed from a `qualified_identifier`) yields the same canonical name as its in-class declaration, a dedup pass **merges a header's method prototype with its `.cpp` definition** into a single symbol (the definition wins for file/line and carries the call-graph edges). Base classes become `implements` edges; method bodies are walked for `calls`/`instantiates` edges; quoted `#include "x.h"` becomes a `dependency` resolved to the declaring module, while system `<...>` includes are skipped. Templates are unwrapped to their inner declaration and flagged `templated`, and the walker descends through `#if`/`#ifdef` preprocessor guards (so code wrapped in `#if defined(HAVE_*)` and headers behind include guards are still extracted). Like the other AST extractors, it walks each function/method body for complexity metrics — `cyclomatic`, `loop_depth`, `loop_count`, `calls_in_loop`, and `recursive_self` (counting `for`/`while`/`do-while`/range-`for` and STL-algorithm lambdas like `for_each`/`transform` as loops) — which the enterprise `analyze_performance` tool consumes. *Limitation:* header/source merging relies on the `.h` and `.cpp` living in the same directory (the common layout); split `include/` + `src/` trees are not merged.
476500

501+
**PHP** is parsed with tree-sitter (the `LanguagePHP` grammar, which tolerates the HTML/`<?php` interleaving of WordPress templates). Classes, interfaces, traits, enums (and their cases), top-level functions, methods, class constants, and properties become symbol facts. Type names are fully qualified with the file's `namespace` (`App\Models\Order`), members use PHP's native `Class::method` / `Class::$prop` notation, and global symbols keep their bare name (the common WordPress case). `extends`/`implements` and in-class `use SomeTrait;` become `implements` edges; method/function bodies are walked for `calls` (global-function, static `Class::method`, and bare instance-method targets), `instantiates` (`new X`), and the standard complexity metrics (`cyclomatic`, `loop_depth`, `loop_count`, `calls_in_loop`, `recursive_self`). `use Foo\Bar;` imports become `dependency` facts; a resolve pass builds a project-wide FQN→module index and turns namespaced references (inheritance, trait use, static calls, instantiations) and resolvable imports into internal module-coupling edges, classifying the rest as external — so PHP participates in `traverse`, `find_path`, and `impact_analysis`. **Outbound HTTP-client detection** emits a client-role `route` fact for each call made through Guzzle (`$client->get('/x')`, `$client->request('POST', '/x')`), the Laravel `Http` facade (`Http::get(...)`, including `Http::withToken(...)->get(...)` chains), Symfony's `HttpClient` (`$httpClient->request(...)`), raw cURL (`curl_setopt($ch, CURLOPT_URL, …)`, `curl_init`), and `file_get_contents` — relative paths are kept (with a `target_hint` inferred from any nearby base-URL env var), while absolute `http(s)://` URLs and interpolated/concatenated paths are skipped. **Framework route DSLs** add server-role `route` facts. **WordPress awareness** (enabled when a `wp-load.php`/`wp-settings.php`/`wp-config.php` marker is present) emits a `route` fact per hook call: `add_action`/`add_filter` registrations (carrying the `callback` target), the `do_action`/`apply_filters` hook points, and `register_rest_route` endpoints — dynamic (interpolated/variable) hook names are skipped. **Laravel** (detected via `laravel/framework` in `composer.json`, an `artisan` file, or a `routes/web.php`|`api.php`) parses the `Route::` DSL in `routes/*.php`: verb registrations (`Route::get/post/…`), `Route::match`/`any`, `Route::resource`/`apiResource` (expanded into their REST actions), nested group prefixes (both `Route::group(['prefix' => …], …)` and the fluent `Route::prefix('x')->…->group(…)`), and the `->name(…)` modifier — handlers are normalized to `Controller::method`. **Symfony** (detected via `symfony/framework-bundle`, or `bin/console` + `config/`) reads routes from PHP 8 `#[Route('/path', methods: ['GET'], name: '…')]` attributes (a class-level attribute prefixes its methods; `methods` accepts string literals or `Request::METHOD_*` constants) and legacy `@Route(…)` docblock annotations, plus YAML (`config/routes.yaml`, `config/routes/*.yaml`) and XML route configuration — these config files are scanned directly from disk, independently of the main walker (the same approach the OpenAPI extractor uses), so they are found even when `*.yaml` is globally ignored, as the bundled configs do. Once emitted, these client/server routes flow through the cross-repo linker and the `unused-routes` explainer like every other language. *Scope:* Symfony route imports/`resource` includes and bundle-local route configs (`**/Resources/config/routes.*`) are not expanded/discovered.
502+
477503
---
478504

479505
## Output artifacts

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Cursor (add to `mcp.json`):
3737
3838
Done. Your agent now has a precise structural map of your code. For configuration options, multi-repo setup, and what to ask next, see [Quick start](#quick-start) below.
3939

40-
**Supported languages:** Go · JavaScript · TypeScript · Python · Java · Kotlin · Swift · Ruby · C++ · Vue · Svelte · OpenAPI — with framework awareness (Next.js, Nuxt, SvelteKit, FastAPI, Django, Spring, Rails, SwiftUI, Jetpack Compose, …)
40+
**Supported languages:** Go · JavaScript · TypeScript · Python · Java · Kotlin · Swift · Ruby · C++ · PHP · Vue · Svelte · OpenAPI — with framework awareness (Next.js, Nuxt, SvelteKit, FastAPI, Django, Spring, Rails, Laravel, Symfony, SwiftUI, Jetpack Compose, WordPress, …)
4141

4242
---
4343

@@ -222,11 +222,12 @@ Working across several repos? Generate the first, then add the rest with append
222222
| Swift | `Package.swift`, `.xcodeproj`, `.xcworkspace` (SwiftUI / UIKit aware) |
223223
| Ruby | `Gemfile` (Rails / ActiveRecord / Packwerk aware) |
224224
| C++ | `.cpp`/`.hpp`/… source or `CMakeLists.txt`/`Makefile` + header (header/source method merging, namespaces, templates) |
225+
| PHP | `composer.json`, WordPress markers, or any `.php` source (WordPress / Laravel / Symfony route + outbound HTTP-client aware) |
225226
| OpenAPI | any spec with an `openapi:` / `swagger:` key |
226227

227228
Framework- and platform-specific detection for each language is described in **[ARCHITECTURE.md → Supported languages](ARCHITECTURE.md#supported-languages)**.
228229

229-
> Python and Ruby are parsed with tree-sitter and contribute call and dependency edges to the graph, so `traverse`, `find_path`, and `impact_analysis` reach into them — not just modules and routes.
230+
> Python, Ruby, and PHP are parsed with tree-sitter and contribute call and dependency edges to the graph, so `traverse`, `find_path`, and `impact_analysis` reach into them — not just modules and routes.
230231
231232
---
232233

examples/full.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# - swift (detection: Package.swift, .xcodeproj, or .xcworkspace)
1313
# - ruby (detection: Gemfile)
1414
# - cpp (detection: .cpp/.hpp/... or CMakeLists.txt/Makefile + header)
15+
# - php (detection: composer.json, WordPress markers, or any .php source)
1516

1617
repo: "."
1718
ignore:
@@ -92,6 +93,7 @@ extractors:
9293
- typescript
9394
- swift
9495
- ruby
96+
- php
9597
explainers:
9698
- cycles
9799
- layers

examples/php.yaml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# enola configuration for a PHP project (plain PHP, Composer, or WordPress).
2+
#
3+
# Detection: The PHP extractor activates when a composer.json, a WordPress
4+
# bootstrap file (wp-load.php / wp-settings.php / wp-config.php), or any
5+
# .php source (within 3 directory levels) is present.
6+
# Features: Classes, interfaces, traits, enums (and cases), functions, methods,
7+
# class constants and properties (namespace-qualified "Ns\Class::member"
8+
# naming; global symbols keep their bare name). Inheritance and trait-use
9+
# (implements edges), a call graph (calls / instantiations), `use` import
10+
# dependencies resolved into internal module-coupling edges, and per-
11+
# function complexity.
12+
# Outbound HTTP-client calls become client routes: Guzzle, the Laravel
13+
# Http facade, Symfony HttpClient, cURL (CURLOPT_URL), and
14+
# file_get_contents (relative paths only; absolute URLs skipped).
15+
# Framework route DSLs become server routes:
16+
# - WordPress: add_action / add_filter (with their callback),
17+
# do_action / apply_filters, register_rest_route.
18+
# - Laravel: Route::get/post/… , match / any, resource / apiResource
19+
# (expanded), nested group prefixes, and ->name(...) — in routes/*.php.
20+
# - Symfony: #[Route(...)] attributes / @Route annotations on
21+
# controllers, plus YAML/XML config (config/routes.yaml, config/routes/).
22+
23+
repo: "."
24+
ignore:
25+
# VCS and tooling
26+
- ".git/**"
27+
- ".enola/**"
28+
# Composer / package dependencies
29+
- "vendor/**"
30+
- "**/vendor/**"
31+
- "node_modules/**"
32+
- "**/node_modules/**"
33+
# WordPress uploads and caches (data, not code)
34+
- "wp-content/uploads/**"
35+
- "wp-content/cache/**"
36+
- "**/cache/**"
37+
# Build / minified assets
38+
- "**/*.min.js"
39+
- "**/*.min.css"
40+
- "build/**"
41+
- "dist/**"
42+
# Tests and fixtures (uncomment to exclude)
43+
# - "tests/**"
44+
# - "**/*Test.php"
45+
# Documentation / config / data
46+
- "**/*.md"
47+
extractors:
48+
- php
49+
explainers:
50+
- cycles
51+
- layers
52+
- crossrepo
53+
- coverage
54+
renderers:
55+
- llm_context
56+
output:
57+
dir: ".enola"
58+
max_context_tokens: 16000

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/tree-sitter/go-tree-sitter v0.24.0
99
github.com/tree-sitter/tree-sitter-cpp v0.22.4-0.20240818224355-b1a4e2b25148
1010
github.com/tree-sitter/tree-sitter-java v0.21.1-0.20240824015150-576d8097e495
11+
github.com/tree-sitter/tree-sitter-php v0.22.9-0.20240819002312-a552625b56c1
1112
github.com/tree-sitter/tree-sitter-python v0.23.6
1213
github.com/tree-sitter/tree-sitter-ruby v0.21.1-0.20240818211811-7dbc1e2d0e2d
1314
github.com/tree-sitter/tree-sitter-typescript v0.23.2

internal/config/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func Default() *Config {
6868
"**/Pods/**",
6969
"**/.gradle/**",
7070
},
71-
Extractors: []string{"cpp", "go", "java", "kotlin", "openapi", "python", "typescript", "swift", "ruby"},
71+
Extractors: []string{"cpp", "go", "java", "kotlin", "openapi", "php", "python", "typescript", "swift", "ruby"},
7272
Explainers: []string{"cycles", "layers", "crossrepo", "coverage", "unused-routes", "god-class", "hotspots", "dependency-depth", "exported-surface", "complexity-outliers"},
7373
Renderers: []string{"llm_context"},
7474
Output: OutputConfig{

internal/engine/cache.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import (
1818
// v2: Swift URLSession extractor precision (file-URL exclusion, interpolation fix).
1919
// v3: Python route facts use method/role/bare-path Name (was http_method, verb-in-name).
2020
// v4: Java HTTP client detection (RestTemplate call sites + @FeignClient interfaces).
21-
const cacheVersion = "v4"
21+
// v5: PHP HTTP client detection + Laravel/Symfony route DSLs (attributes, YAML/XML config).
22+
const cacheVersion = "v5"
2223

2324
// extractorCache holds per-extractor facts keyed by a content hash of the files
2425
// the extractor depends on. It is loaded from disk at the start of a snapshot and

0 commit comments

Comments
 (0)