play.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. /**
  2. * Play Framework (Scala/Java) resolver.
  3. *
  4. * Play declares HTTP routes in a dedicated `conf/routes` file (and included
  5. * `conf/*.routes`), Rails-style:
  6. *
  7. * GET /computers controllers.Application.list(p: Int ?= 0)
  8. * POST /computers controllers.Application.save
  9. * GET /assets/*file controllers.Assets.versioned(path = "/public", file: Asset)
  10. *
  11. * The file is extensionless, so the file walk only indexes it because
  12. * `isPlayRoutesFile` (grammars.ts) opts it in; it's processed through the
  13. * no-grammar path and this resolver extracts the routes. Each route references
  14. * its handler as `Controller.method` (the package prefix is dropped), resolved
  15. * to the action method in the controller class.
  16. */
  17. import { Node } from '../../types';
  18. import { FrameworkResolver, ResolutionContext, ResolvedRef, UnresolvedRef } from '../types';
  19. import { isPlayRoutesFile } from '../../extraction/grammars';
  20. const ROUTE_LINE = /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(\S+)\s+(.+)$/;
  21. const METHOD_KINDS = new Set(['method', 'function']);
  22. const CLASS_KINDS = new Set(['class']);
  23. export const playResolver: FrameworkResolver = {
  24. name: 'play',
  25. // `yaml` so this resolver runs on conf/routes (detectLanguage maps it to yaml);
  26. // `scala`/`java` so it's active in Play projects of either language.
  27. languages: ['scala', 'java', 'yaml'],
  28. detect(context: ResolutionContext): boolean {
  29. const buildSbt = context.readFile('build.sbt');
  30. if (buildSbt && /playframework|"play"|sbt-plugin|PlayScala|PlayJava/i.test(buildSbt)) return true;
  31. if (context.fileExists('conf/routes')) return true;
  32. if (context.fileExists('conf/application.conf')) return true;
  33. return false;
  34. },
  35. // The handler is `Controller.method` (a class-qualified action), which names no
  36. // bare declared symbol, so resolveOne's pre-filter could drop it — claim it.
  37. claimsReference(name: string): boolean {
  38. return /^[A-Za-z_]\w*\.[A-Za-z_]\w*$/.test(name);
  39. },
  40. resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
  41. const m = ref.referenceName.match(/^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/);
  42. if (!m) return null;
  43. const [, className, methodName] = m;
  44. const classNodes = context.getNodesByName(className!).filter((n) => CLASS_KINDS.has(n.kind));
  45. for (const cls of classNodes) {
  46. const method = context
  47. .getNodesInFile(cls.filePath)
  48. .find((n) => METHOD_KINDS.has(n.kind) && n.name === methodName);
  49. if (method) {
  50. return { original: ref, targetNodeId: method.id, confidence: 0.9, resolvedBy: 'framework' };
  51. }
  52. }
  53. return null;
  54. },
  55. extract(filePath: string, content: string): { nodes: Node[]; references: UnresolvedRef[] } {
  56. if (!isPlayRoutesFile(filePath)) return { nodes: [], references: [] };
  57. const nodes: Node[] = [];
  58. const references: UnresolvedRef[] = [];
  59. const now = Date.now();
  60. const lines = content.split('\n');
  61. for (let i = 0; i < lines.length; i++) {
  62. const line = lines[i]!.trim();
  63. // Skip comments and `->` route includes (a sub-router mount, not an action).
  64. if (!line || line.startsWith('#') || line.startsWith('->')) continue;
  65. const m = line.match(ROUTE_LINE);
  66. if (!m) continue;
  67. const [, method, routePath, action] = m;
  68. // action: `controllers.Application.list(p: Int ?= 0)` → drop args, keep the
  69. // last `Controller.method` segment (package prefix is irrelevant for lookup).
  70. const fqn = action!.split('(')[0]!.trim();
  71. const parts = fqn.split('.').filter(Boolean);
  72. if (parts.length < 2) continue;
  73. const handlerRef = parts.slice(-2).join('.'); // Application.list
  74. const lineNum = i + 1;
  75. const routeNode: Node = {
  76. id: `route:${filePath}:${lineNum}:${method}:${routePath}`,
  77. kind: 'route',
  78. name: `${method} ${routePath}`,
  79. qualifiedName: `${filePath}::${method}:${routePath}`,
  80. filePath,
  81. startLine: lineNum,
  82. endLine: lineNum,
  83. startColumn: 0,
  84. endColumn: 0,
  85. language: 'scala',
  86. updatedAt: now,
  87. };
  88. nodes.push(routeNode);
  89. references.push({
  90. fromNodeId: routeNode.id,
  91. referenceName: handlerRef,
  92. referenceKind: 'references',
  93. line: lineNum,
  94. column: 0,
  95. filePath,
  96. language: 'scala',
  97. });
  98. }
  99. return { nodes, references };
  100. },
  101. };