mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-05 08:53:34 +08:00
Detection only looked at the root (`build.sbt`, `conf/routes`, `conf/application.conf`); playframework/play-samples keeps each project's `conf/routes` in its own directory and had no routes. Any indexed Play routes file now counts. play-samples 0 → 139 routes (+108 edges); root-level Play repos are unaffected. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
115 lines
4.5 KiB
TypeScript
115 lines
4.5 KiB
TypeScript
/**
|
|
* Play Framework (Scala/Java) resolver.
|
|
*
|
|
* Play declares HTTP routes in a dedicated `conf/routes` file (and included
|
|
* `conf/*.routes`), Rails-style:
|
|
*
|
|
* GET /computers controllers.Application.list(p: Int ?= 0)
|
|
* POST /computers controllers.Application.save
|
|
* GET /assets/*file controllers.Assets.versioned(path = "/public", file: Asset)
|
|
*
|
|
* The file is extensionless, so the file walk only indexes it because
|
|
* `isPlayRoutesFile` (grammars.ts) opts it in; it's processed through the
|
|
* no-grammar path and this resolver extracts the routes. Each route references
|
|
* its handler as `Controller.method` (the package prefix is dropped), resolved
|
|
* to the action method in the controller class.
|
|
*/
|
|
|
|
import { Node } from '../../types';
|
|
import { FrameworkResolver, ResolutionContext, ResolvedRef, UnresolvedRef } from '../types';
|
|
import { isPlayRoutesFile } from '../../extraction/grammars';
|
|
|
|
const ROUTE_LINE = /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(\S+)\s+(.+)$/;
|
|
const METHOD_KINDS = new Set(['method', 'function']);
|
|
const CLASS_KINDS = new Set(['class']);
|
|
|
|
export const playResolver: FrameworkResolver = {
|
|
name: 'play',
|
|
// `yaml` so this resolver runs on conf/routes (detectLanguage maps it to yaml);
|
|
// `scala`/`java` so it's active in Play projects of either language.
|
|
languages: ['scala', 'java', 'yaml'],
|
|
|
|
detect(context: ResolutionContext): boolean {
|
|
const buildSbt = context.readFile('build.sbt');
|
|
if (buildSbt && /playframework|"play"|sbt-plugin|PlayScala|PlayJava/i.test(buildSbt)) return true;
|
|
if (context.fileExists('conf/routes')) return true;
|
|
if (context.fileExists('conf/application.conf')) return true;
|
|
// A repository of Play projects (playframework/play-samples) keeps each
|
|
// one's `conf/routes` in its own directory.
|
|
return context.getAllFiles().some((f) => isPlayRoutesFile(f));
|
|
},
|
|
|
|
// The handler is `Controller.method` (a class-qualified action), which names no
|
|
// bare declared symbol, so resolveOne's pre-filter could drop it — claim it.
|
|
claimsReference(name: string): boolean {
|
|
return /^[A-Za-z_]\w*\.[A-Za-z_]\w*$/.test(name);
|
|
},
|
|
|
|
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
|
const m = ref.referenceName.match(/^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/);
|
|
if (!m) return null;
|
|
const [, className, methodName] = m;
|
|
const classNodes = context.getNodesByName(className!).filter((n) => CLASS_KINDS.has(n.kind));
|
|
for (const cls of classNodes) {
|
|
const method = context
|
|
.getNodesInFile(cls.filePath)
|
|
.find((n) => METHOD_KINDS.has(n.kind) && n.name === methodName);
|
|
if (method) {
|
|
return { original: ref, targetNodeId: method.id, confidence: 0.9, resolvedBy: 'framework' };
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
extract(filePath: string, content: string): { nodes: Node[]; references: UnresolvedRef[] } {
|
|
if (!isPlayRoutesFile(filePath)) return { nodes: [], references: [] };
|
|
const nodes: Node[] = [];
|
|
const references: UnresolvedRef[] = [];
|
|
const now = Date.now();
|
|
|
|
const lines = content.split('\n');
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i]!.trim();
|
|
// Skip comments and `->` route includes (a sub-router mount, not an action).
|
|
if (!line || line.startsWith('#') || line.startsWith('->')) continue;
|
|
const m = line.match(ROUTE_LINE);
|
|
if (!m) continue;
|
|
const [, method, routePath, action] = m;
|
|
|
|
// action: `controllers.Application.list(p: Int ?= 0)` → drop args, keep the
|
|
// last `Controller.method` segment (package prefix is irrelevant for lookup).
|
|
const fqn = action!.split('(')[0]!.trim();
|
|
const parts = fqn.split('.').filter(Boolean);
|
|
if (parts.length < 2) continue;
|
|
const handlerRef = parts.slice(-2).join('.'); // Application.list
|
|
|
|
const lineNum = i + 1;
|
|
const routeNode: Node = {
|
|
id: `route:${filePath}:${lineNum}:${method}:${routePath}`,
|
|
kind: 'route',
|
|
name: `${method} ${routePath}`,
|
|
qualifiedName: `${filePath}::${method}:${routePath}`,
|
|
filePath,
|
|
startLine: lineNum,
|
|
endLine: lineNum,
|
|
startColumn: 0,
|
|
endColumn: 0,
|
|
language: 'scala',
|
|
updatedAt: now,
|
|
};
|
|
nodes.push(routeNode);
|
|
references.push({
|
|
fromNodeId: routeNode.id,
|
|
referenceName: handlerRef,
|
|
referenceKind: 'references',
|
|
line: lineNum,
|
|
column: 0,
|
|
filePath,
|
|
language: 'scala',
|
|
});
|
|
}
|
|
|
|
return { nodes, references };
|
|
},
|
|
};
|