cross-file-visibility.test.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. /**
  2. * A definition the language makes file-local is not a candidate for a
  3. * cross-file name match: a C `static`, a Kotlin `private fun`, a Go unexported
  4. * identifier in another package, a Rust non-`pub` item outside its module
  5. * subtree. Each case pairs the invisible shape with the visible one of
  6. * identical form, so the assertion discriminates on visibility alone.
  7. */
  8. import { describe, it, expect, afterEach } from 'vitest';
  9. import * as fs from 'fs';
  10. import * as os from 'os';
  11. import * as path from 'path';
  12. import CodeGraph from '../src/index';
  13. let tempDir: string;
  14. let cg: CodeGraph | null = null;
  15. function project(files: Record<string, string>): void {
  16. tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-visibility-'));
  17. for (const [rel, content] of Object.entries(files)) {
  18. const abs = path.join(tempDir, rel);
  19. fs.mkdirSync(path.dirname(abs), { recursive: true });
  20. fs.writeFileSync(abs, content);
  21. }
  22. }
  23. /** `calls` targets of the function named `caller`, as `file:name` strings. */
  24. async function calleesOf(caller: string): Promise<string[]> {
  25. cg = await CodeGraph.init(tempDir, { index: true });
  26. cg.resolveReferences();
  27. const from = cg.getNodesByKind('function').concat(cg.getNodesByKind('method')).find((n) => n.name === caller)!;
  28. expect(from).toBeDefined();
  29. return cg
  30. .getOutgoingEdges(from.id)
  31. .filter((e) => e.kind === 'calls')
  32. .map((e) => cg!.getNode(e.target))
  33. .filter((n): n is NonNullable<typeof n> => !!n)
  34. .map((n) => `${n.filePath}:${n.name}`);
  35. }
  36. afterEach(() => {
  37. cg?.close();
  38. cg = null;
  39. fs.rmSync(tempDir, { recursive: true, force: true });
  40. });
  41. describe('C: a static function is local to its translation unit', () => {
  42. it('does not resolve a call onto a static in another file', async () => {
  43. project({
  44. 'core.c': 'void coreRun(void)\n{\n usbGetDescriptor();\n}\n',
  45. 'usb_audio.c': 'static void usbGetDescriptor(void)\n{\n}\n',
  46. });
  47. expect(await calleesOf('coreRun')).not.toContain('usb_audio.c:usbGetDescriptor');
  48. });
  49. it('still resolves onto a non-static function in another file', async () => {
  50. project({
  51. 'core.c': 'void coreRun(void)\n{\n usbGetDescriptor();\n}\n',
  52. 'usb_audio.c': 'void usbGetDescriptor(void)\n{\n}\n',
  53. });
  54. expect(await calleesOf('coreRun')).toContain('usb_audio.c:usbGetDescriptor');
  55. });
  56. it('keeps a static inline defined in a header: it lives in every unit that includes it', async () => {
  57. project({
  58. 'protocol.h': 'static inline void mav_put_char(char *buf, char c)\n{\n buf[0] = c;\n}\n',
  59. 'core.c': '#include "protocol.h"\n\nvoid coreRun(char *b)\n{\n mav_put_char(b, 0);\n}\n',
  60. });
  61. expect(await calleesOf('coreRun')).toContain('protocol.h:mav_put_char');
  62. });
  63. it('keeps a same-file static, whichever line the keyword is on', async () => {
  64. project({
  65. 'core.c': 'static void\nhelper(void)\n{\n}\n\nvoid coreRun(void)\n{\n helper();\n}\n',
  66. 'other.c': 'static void helper(void)\n{\n}\n',
  67. });
  68. expect(await calleesOf('coreRun')).toEqual(['core.c:helper']);
  69. });
  70. });
  71. describe('Kotlin: a private function is class- or file-local', () => {
  72. it('does not resolve an SDK-style call onto another file\'s private fun', async () => {
  73. project({
  74. 'Budget.kt': 'class Budget {\n private fun apply(bps: Long): Long = bps\n}\n',
  75. 'Main.kt': 'class Main {\n fun onCreate(editor: Editor) {\n editor.apply()\n }\n}\n',
  76. });
  77. expect(await calleesOf('onCreate')).not.toContain('Budget.kt:apply');
  78. });
  79. it('still resolves onto a public fun in another file', async () => {
  80. project({
  81. 'Budget.kt': 'class Budget {\n fun apply(bps: Long): Long = bps\n}\n',
  82. 'Main.kt': 'class Main {\n fun onCreate(budget: Budget) {\n budget.apply(1L)\n }\n}\n',
  83. });
  84. expect(await calleesOf('onCreate')).toContain('Budget.kt:apply');
  85. });
  86. });
  87. describe('Go: an unexported identifier is package-local', () => {
  88. it('does not resolve a call onto an unexported func in another package', async () => {
  89. project({
  90. 'cmd/probe/main.go': 'package main\n\nfunc fail(msg string) {}\n',
  91. 'server/turn.go': 'package server\n\nfunc Run() {\n\tfail("x")\n}\n',
  92. });
  93. expect(await calleesOf('Run')).not.toContain('cmd/probe/main.go:fail');
  94. });
  95. it('still resolves within the package and onto an exported func elsewhere', async () => {
  96. project({
  97. 'server/util.go': 'package server\n\nfunc fail(msg string) {}\n',
  98. 'server/turn.go': 'package server\n\nfunc Run() {\n\tfail("x")\n\tReport()\n}\n',
  99. 'report/report.go': 'package report\n\nfunc Report() {}\n',
  100. });
  101. const callees = await calleesOf('Run');
  102. expect(callees).toContain('server/util.go:fail');
  103. expect(callees).toContain('report/report.go:Report');
  104. });
  105. });
  106. describe('Rust: a non-pub item is visible to its module subtree only', () => {
  107. it('does not resolve a sibling module\'s private fn, nor another crate\'s', async () => {
  108. project({
  109. 'src/main.rs': 'mod util;\nmod net;\nfn main() {}\n',
  110. 'src/util.rs': 'fn count() -> usize { 0 }\n',
  111. 'src/net.rs': 'pub fn run() -> usize {\n count()\n}\n',
  112. });
  113. expect(await calleesOf('run')).not.toContain('src/util.rs:count');
  114. });
  115. it('keeps a trait-impl method, which has the trait\'s visibility', async () => {
  116. project({
  117. 'src/main.rs': 'mod shape;\nmod draw;\nfn main() {}\n',
  118. 'src/shape.rs': 'pub struct Circle;\npub trait Area { fn area(&self) -> f64; }\nimpl Area for Circle {\n fn area(&self) -> f64 { 1.0 }\n}\n',
  119. 'src/draw.rs': 'use crate::shape::{Area, Circle};\npub fn render(c: &Circle) -> f64 {\n c.area()\n}\n',
  120. });
  121. expect(await calleesOf('render')).toContain('src/shape.rs:area');
  122. });
  123. it('still resolves a parent module\'s private fn from a child, and any pub fn', async () => {
  124. project({
  125. 'src/main.rs': 'mod net;\nmod util;\nfn main() {}\n',
  126. 'src/net.rs': 'pub mod tcp;\nfn shared() {}\n',
  127. 'src/net/tcp.rs': 'use super::shared;\nuse crate::util::exported;\npub fn open() {\n shared();\n exported();\n}\n',
  128. 'src/util.rs': 'pub fn exported() {}\n',
  129. });
  130. const callees = await calleesOf('open');
  131. expect(callees).toContain('src/net.rs:shared');
  132. expect(callees).toContain('src/util.rs:exported');
  133. });
  134. });