generated-detection.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. /**
  2. * Generated-file detection for symbol-disambiguation down-ranking.
  3. *
  4. * When a query like "Send" matches 17 symbols across protobuf scaffolding,
  5. * test mocks, and the hand-written implementation, the FTS ranker often
  6. * surfaces the generated stubs first because their names are identical
  7. * to the implementation's name (validated empirically on cosmos-sdk —
  8. * see project_go_multi_module_audit memory). Generated stubs frequently
  9. * have no body to trace from, so the agent ends up reading source anyway.
  10. *
  11. * This is a relevance hint consulted at disambiguation time (findSymbol /
  12. * findAllSymbols / explore ranking / codegraph_search formatting), NOT a
  13. * hard filter — generated nodes are still in the graph and remain
  14. * reachable; they just rank LAST when there's a real implementation with
  15. * the same name.
  16. *
  17. * Two signals, deliberately separate:
  18. *
  19. * 1. {@link isGeneratedFile} — PATH only, pure and synchronous. Most
  20. * generated files follow the `<basename>.<tool>.<ext>` convention
  21. * (`.pb.go`, `_grpc.pb.go`, `.g.dart`, `_pb2.py`). Free to call
  22. * anywhere, including in a sort comparator.
  23. *
  24. * 2. {@link hasGeneratedHeader} — CONTENT banner in the file's head. Go's
  25. * own convention is a content marker, not a filename one, so a
  26. * generated `payroll.go` sitting beside hand-written use-cases is
  27. * invisible to (1) — that is issue #1500. Evaluated ONCE at index time
  28. * (the file's content is already in memory for parsing) and persisted
  29. * on the file record as `files.generated`; readers get it from the DB
  30. * rather than re-reading headers per request. See
  31. * GENERATED_CONTENT_PATTERNS below for the banners recognized.
  32. *
  33. * Consumers that have a bounded candidate list should use the DB-backed
  34. * union (`QueryBuilder.getGeneratedPathsAmong` /
  35. * `CodeGraph.getGeneratedFilePaths`) so both signals apply; the path-only
  36. * check remains the fallback for callers with no database in hand and for
  37. * indexes built before the flag existed.
  38. *
  39. * NOTE for future editors: the banner literals quoted in this file sit
  40. * BELOW the header window this detector scans, so the module does not
  41. * classify itself. `generated-detection.test.ts` pins that — if you move
  42. * the pattern table upward, the test fails rather than the repo silently
  43. * demoting its own file.
  44. */
  45. const GENERATED_PATTERNS: ReadonlyArray<RegExp> = [
  46. // Go — protobuf / gRPC / pulsar
  47. /\.pb\.go$/,
  48. /\.pulsar\.go$/,
  49. /_grpc\.pb\.go$/,
  50. // Go — mockgen output. Default emits `mock_<src>.go`; many projects
  51. // (cosmos-sdk uses `expected_*_mocks.go`) rename to `*_mock.go` /
  52. // `*_mocks.go`. Matching either suffix catches both conventions
  53. // without false-positive risk on hand-written sources.
  54. /_mock\.go$/,
  55. /_mocks\.go$/,
  56. /^mock_[^/]+\.go$/,
  57. // TypeScript / JavaScript — common codegen suffixes (Apollo / GraphQL
  58. // codegen, Prisma, Hasura, ts-proto, gRPC-web, swagger-codegen).
  59. /\.generated\.[jt]sx?$/,
  60. /\.gen\.[jt]sx?$/,
  61. /\.pb\.[jt]s$/,
  62. /_pb\.[jt]s$/,
  63. /_grpc_pb\.[jt]s$/,
  64. // Minified bundles vendored into a repo (docs sites, examples). Their
  65. // single-letter symbols make name-based edges pure noise.
  66. /\.min\.m?js$/,
  67. // Python — protobuf / gRPC / openapi-codegen
  68. /_pb2(_grpc)?\.py$/,
  69. /_pb2\.pyi$/,
  70. // C++ — protobuf
  71. /\.pb\.(cc|h)$/,
  72. // C# — protobuf / gRPC (protoc-gen-csharp puts output under obj/ but
  73. // many projects also commit *.g.cs and *Grpc.cs siblings)
  74. /\.g\.cs$/,
  75. /Grpc\.cs$/,
  76. // Java — protobuf / gRPC: protoc-gen-java emits `*OuterClass.java`,
  77. // protoc-gen-grpc-java emits `*Grpc.java`. The XxxImplBase abstract
  78. // class lives inside Xxx*Grpc.java.
  79. /OuterClass\.java$/,
  80. /Grpc\.java$/,
  81. // Swift — protobuf
  82. /\.pb\.swift$/,
  83. // Dart — build_runner / freezed / json_serializable / chopper
  84. /\.g\.dart$/,
  85. /\.freezed\.dart$/,
  86. /\.pb\.dart$/,
  87. /\.pbgrpc\.dart$/,
  88. /\.chopper\.dart$/,
  89. // Rust — common build.rs OUT_DIR outputs are usually outside the source
  90. // tree, but in-tree generated files often use `*.generated.rs`.
  91. /\.generated\.rs$/,
  92. ];
  93. /**
  94. * Whether `filePath` looks like a tool-generated source file based on
  95. * its filename. Path-only — does not read content. The result is a
  96. * relevance hint for disambiguation, not a hard claim.
  97. */
  98. export function isGeneratedFile(filePath: string): boolean {
  99. return GENERATED_PATTERNS.some((p) => p.test(filePath));
  100. }
  101. // =============================================================================
  102. // Content-header detection (#1500)
  103. // =============================================================================
  104. /**
  105. * How much of a file's head to consider "the header". Generous enough for a
  106. * build-tag block + an Apache-2.0 license preamble (~15 lines) sitting above
  107. * the banner, tight enough that a `"// Code generated ... DO NOT EDIT."`
  108. * string constant in the *body* of a code generator's own source can't
  109. * masquerade as a banner.
  110. */
  111. const HEADER_SCAN_CHARS = 8192;
  112. const HEADER_SCAN_LINES = 60;
  113. /**
  114. * Cheap pre-filter run on the header of EVERY indexed file. Every marker
  115. * below contains the stem "generat", so one unanchored scan rejects ~all
  116. * hand-written source before any line splitting happens — this is what keeps
  117. * content detection off the index-time cost budget.
  118. */
  119. const GENERATED_STEM = /generat/i;
  120. /**
  121. * Line-comment leaders across the languages we index. A banner must sit on a
  122. * comment line (or inside an open block comment, tracked below): generators
  123. * always emit theirs as a comment, and requiring it rules out string literals
  124. * and identifiers that merely contain the words.
  125. *
  126. * `--` covers SQL/Haskell/Lua, `%` LaTeX/Erlang/Prolog, `;` Lisp/asm/ini,
  127. * `'` VB, `!` Fortran, `*` a continuation line inside a `/* … *\/` block.
  128. */
  129. const COMMENT_LEADER =
  130. /^\s*(?:\/\/|\/\*+|\*+\/?|#+|--+|<!--|%+|;+|'|!|\(\*|\{-|"""|'''|=begin|<#|@rem\b|rem\b)/i;
  131. /**
  132. * Openers/closers for block comments, so a banner on an unprefixed line
  133. * inside `/* … *\/` (or `<!-- … -->`, or a Python module docstring) still
  134. * counts. Deliberately naive — it only runs over a file's first few dozen
  135. * lines, where a `/*` inside a string literal is vanishingly rare, and the
  136. * worst case of a mis-tracked state is a ranking hint, not a wrong answer.
  137. */
  138. const BLOCK_DELIMS: ReadonlyArray<{ open: string; close: string }> = [
  139. { open: '/*', close: '*/' },
  140. { open: '<!--', close: '-->' },
  141. { open: '"""', close: '"""' },
  142. { open: "'''", close: "'''" },
  143. { open: '=begin', close: '=end' },
  144. { open: '<#', close: '#>' },
  145. ];
  146. /**
  147. * The banners themselves. Each is a real convention emitted by a widely-used
  148. * generator; the list is precision-first, because a false positive silently
  149. * demotes hand-written code in every ranking path.
  150. */
  151. const GENERATED_CONTENT_PATTERNS: ReadonlyArray<RegExp> = [
  152. // Go's codified convention — `^// Code generated .* DO NOT EDIT\.$`, defined
  153. // by `go generate` and honored by gofmt, golangci-lint and GitHub linguist.
  154. // Emitted verbatim by protoc-gen-go, mockgen, sqlc, ent, wire, stringer, and
  155. // by in-house generators like the FKIT CRUD in #1500 — where the file is
  156. // named `payroll.go` and nothing in the PATH gives it away.
  157. /\bcode generated\b.{0,200}?\bdo not edit\b/i,
  158. // protoc's Java/C#/Python banner ("Generated by the protocol buffer
  159. // compiler. DO NOT EDIT!"), ANTLR, Dagger, FlatBuffers, rust-bindgen,
  160. // Xcode asset catalogs, Bazel rules.
  161. /\b(?:automatically |auto[- ]?)?generated (?:by|from|with)\b.{0,200}?\bdo not (?:edit|modify|change)\b/i,
  162. // The `@generated` marker: the JS/TS ecosystem's convention (Relay, GraphQL
  163. // codegen, protobuf-es/Buf, Meta's `@generated SignedSource<<…>>`), also
  164. // what linguist and `git diff` collapse on. Guarded against `foo@generated`
  165. // and `@@generated` so only a standalone tag matches.
  166. /(?:^|[^\p{L}\p{N}_@])@generated\b/u,
  167. // .NET's `<auto-generated>` / `<auto-generated />` doc tag: Roslyn, the
  168. // WinForms designer, T4 templates, protoc-gen-csharp, EF scaffolding.
  169. /<auto-?generated\s*\/?>/i,
  170. // swagger-codegen / OpenAPI Generator ("NOTE: This class is auto generated
  171. // by OpenAPI Generator"), Thrift ("Autogenerated by Thrift Compiler"),
  172. // FlatBuffers ("automatically generated by the FlatBuffers compiler").
  173. // "by" is required — bare "automatically generated" appears in hand-written
  174. // prose ("the table below is automatically generated at runtime").
  175. /\b(?:automatically generated|auto[- ]?generated|autogenerated) by\b/i,
  176. // The "run this command to regenerate" shape: Cloudflare Wrangler
  177. // ("Generated by Wrangler by running `wrangler types` (hash: …)"), and the
  178. // same phrasing used by other CLI-driven emitters. Bare "generated by" is
  179. // deliberately NOT enough — it is ordinary prose — so the reproduction
  180. // instruction is the discriminator: the banner must name a tool AND then
  181. // say `by running`, i.e. TWO separate "by" clauses. That rules out
  182. // "the report is generated by running the nightly job", which has only one.
  183. /\bgenerated by\s+\S.{0,80}?\bby running\b/i,
  184. // Self-declaring in-house banners that name no tool.
  185. /\bthis (?:file|class|code|module) (?:is|was) (?:auto[- ]?)?generated\b/i,
  186. // The reverse ordering: "DO NOT EDIT — this is a generated file".
  187. /\bdo not (?:edit|modify)\b.{0,120}?\b(?:auto[- ]?generated|generated file|generated code)\b/i,
  188. ];
  189. /**
  190. * Whether the head of `content` carries a recognized machine-generation
  191. * banner. Bounded to {@link HEADER_SCAN_CHARS} / {@link HEADER_SCAN_LINES},
  192. * and the marker must sit on a comment line — a generator's own source, which
  193. * holds the banner as a string constant in its body, is not flagged.
  194. *
  195. * Called once per file during extraction (content is already in memory), NOT
  196. * per query: the verdict is persisted on the file record.
  197. */
  198. export function hasGeneratedHeader(content: string): boolean {
  199. if (!content) return false;
  200. const head = content.length > HEADER_SCAN_CHARS ? content.slice(0, HEADER_SCAN_CHARS) : content;
  201. // Fast reject for ~every hand-written file: no line splitting, no allocation
  202. // (V8 keeps `head` as a sliced view of `content`).
  203. if (!GENERATED_STEM.test(head)) return false;
  204. const lines = head.split('\n');
  205. const limit = Math.min(lines.length, HEADER_SCAN_LINES);
  206. let openBlock: (typeof BLOCK_DELIMS)[number] | null = null;
  207. for (let i = 0; i < limit; i++) {
  208. const line = lines[i]!;
  209. const inBlock = openBlock !== null;
  210. if (inBlock || COMMENT_LEADER.test(line)) {
  211. for (const pattern of GENERATED_CONTENT_PATTERNS) {
  212. if (pattern.test(line)) return true;
  213. }
  214. }
  215. // Advance the block-comment state AFTER testing, so the opening line of a
  216. // `/* Code generated … */` block is itself matched by the leader rule.
  217. if (openBlock) {
  218. if (line.includes(openBlock.close)) openBlock = null;
  219. continue;
  220. }
  221. for (const delim of BLOCK_DELIMS) {
  222. const at = line.indexOf(delim.open);
  223. if (at < 0) continue;
  224. // Same-line close (`/* … */`, a one-line docstring) leaves no open block.
  225. if (line.indexOf(delim.close, at + delim.open.length) < 0) openBlock = delim;
  226. break;
  227. }
  228. }
  229. return false;
  230. }
  231. /**
  232. * The union signal: path convention OR content banner. This is what the
  233. * indexer persists to `files.generated`.
  234. */
  235. export function detectGeneratedFile(filePath: string, content: string): boolean {
  236. return isGeneratedFile(filePath) || hasGeneratedHeader(content);
  237. }