1
0

extension-mapping.test.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. /**
  2. * Custom extension → language mapping (#906).
  3. *
  4. * A project can map non-standard file extensions to a supported language via a
  5. * committed `codegraph.json` at the repo root, so files that would otherwise be
  6. * silently skipped get indexed under the right grammar. These tests cover the
  7. * two choke-point functions (detectLanguage / isSourceFile) honoring an override
  8. * map, the loader's validation/normalization/caching of `codegraph.json`, and a
  9. * full index proving a custom-extension file is actually extracted — while the
  10. * zero-config path stays byte-identical (the file is NOT indexed without config).
  11. */
  12. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  13. import * as fs from 'node:fs';
  14. import * as path from 'node:path';
  15. import * as os from 'node:os';
  16. import { CodeGraph } from '../src';
  17. import { detectLanguage, isSourceFile } from '../src/extraction/grammars';
  18. import { loadExtensionOverrides, clearProjectConfigCache } from '../src/project-config';
  19. describe('custom extension → language mapping (#906)', () => {
  20. describe('detectLanguage / isSourceFile overrides argument', () => {
  21. it('maps a custom extension only when present in the overrides', () => {
  22. expect(detectLanguage('a/b.foo')).toBe('unknown');
  23. expect(isSourceFile('a/b.foo')).toBe(false);
  24. expect(detectLanguage('a/b.foo', undefined, { '.foo': 'typescript' })).toBe('typescript');
  25. expect(isSourceFile('a/b.foo', { '.foo': 'typescript' })).toBe(true);
  26. });
  27. it('lets a user mapping take precedence over a built-in extension', () => {
  28. expect(detectLanguage('x.h')).toBe('c');
  29. expect(detectLanguage('x.h', undefined, { '.h': 'cpp' })).toBe('cpp');
  30. });
  31. it('is byte-identical to zero-config behavior when no overrides are passed', () => {
  32. expect(detectLanguage('x.ts')).toBe('typescript');
  33. expect(detectLanguage('x.py')).toBe('python');
  34. expect(isSourceFile('x.ts')).toBe(true);
  35. expect(isSourceFile('x.unknownext')).toBe(false);
  36. });
  37. });
  38. describe('loadExtensionOverrides (codegraph.json)', () => {
  39. let dir: string;
  40. beforeEach(() => {
  41. dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-extmap-'));
  42. clearProjectConfigCache();
  43. });
  44. afterEach(() => {
  45. clearProjectConfigCache();
  46. fs.rmSync(dir, { recursive: true, force: true });
  47. });
  48. const writeConfig = (obj: unknown) =>
  49. fs.writeFileSync(
  50. path.join(dir, 'codegraph.json'),
  51. typeof obj === 'string' ? obj : JSON.stringify(obj)
  52. );
  53. it('returns an empty map when there is no codegraph.json', () => {
  54. expect(loadExtensionOverrides(dir)).toEqual({});
  55. });
  56. it('loads and validates a well-formed extensions map', () => {
  57. writeConfig({ extensions: { '.foo': 'typescript', '.bar': 'python' } });
  58. expect(loadExtensionOverrides(dir)).toEqual({ '.foo': 'typescript', '.bar': 'python' });
  59. });
  60. it('normalizes keys (adds a leading dot, lowercases)', () => {
  61. writeConfig({ extensions: { foo: 'lua', '.BAR': 'go' } });
  62. expect(loadExtensionOverrides(dir)).toEqual({ '.foo': 'lua', '.bar': 'go' });
  63. });
  64. it('skips entries whose target is not a supported language', () => {
  65. writeConfig({ extensions: { '.foo': 'typescript', '.bad': 'pyhton', '.x': 'unknown' } });
  66. expect(loadExtensionOverrides(dir)).toEqual({ '.foo': 'typescript' });
  67. });
  68. it('skips multi-part and otherwise unusable extension keys', () => {
  69. writeConfig({ extensions: { '.d.ts': 'typescript', 'a/b': 'go', '.': 'lua', '.ok': 'rust' } });
  70. expect(loadExtensionOverrides(dir)).toEqual({ '.ok': 'rust' });
  71. });
  72. it('ignores malformed JSON without throwing', () => {
  73. writeConfig('{ not: valid json ');
  74. expect(loadExtensionOverrides(dir)).toEqual({});
  75. });
  76. it('ignores a non-object extensions field', () => {
  77. writeConfig({ extensions: 'nope' });
  78. expect(loadExtensionOverrides(dir)).toEqual({});
  79. });
  80. it('picks up a changed config (mtime-invalidated cache)', () => {
  81. writeConfig({ extensions: { '.foo': 'typescript' } });
  82. expect(loadExtensionOverrides(dir)).toEqual({ '.foo': 'typescript' });
  83. writeConfig({ extensions: { '.foo': 'go' } });
  84. // Force a distinct mtime in case the filesystem clock is coarse.
  85. const future = new Date(Date.now() + 2000);
  86. fs.utimesSync(path.join(dir, 'codegraph.json'), future, future);
  87. expect(loadExtensionOverrides(dir)).toEqual({ '.foo': 'go' });
  88. });
  89. });
  90. describe('indexAll honors codegraph.json end-to-end', () => {
  91. let dir: string;
  92. beforeEach(() => {
  93. dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-extmap-idx-'));
  94. clearProjectConfigCache();
  95. });
  96. afterEach(() => {
  97. clearProjectConfigCache();
  98. fs.rmSync(dir, { recursive: true, force: true });
  99. });
  100. const write = (rel: string, body: string) => {
  101. const p = path.join(dir, rel);
  102. fs.mkdirSync(path.dirname(p), { recursive: true });
  103. fs.writeFileSync(p, body);
  104. };
  105. const indexAndQuery = async () => {
  106. const cg = await CodeGraph.init(dir, { silent: true });
  107. await cg.indexAll();
  108. const db = (cg as any).db.db;
  109. const nodes = db
  110. .prepare('SELECT name, kind, file_path, language FROM nodes WHERE file_path = ?')
  111. .all('widget.foo');
  112. const files = db
  113. .prepare('SELECT path, language FROM files WHERE path = ?')
  114. .all('widget.foo');
  115. cg.close?.();
  116. return { nodes, files };
  117. };
  118. const SOURCE = 'export function widgetHandler(x: number): number { return x + 1; }\n';
  119. it('indexes a custom-extension file mapped to a supported language', async () => {
  120. write('codegraph.json', JSON.stringify({ extensions: { '.foo': 'typescript' } }));
  121. write('widget.foo', SOURCE);
  122. const { nodes, files } = await indexAndQuery();
  123. expect(files.length).toBe(1);
  124. expect(files[0].language).toBe('typescript');
  125. expect(nodes.some((n: any) => n.name === 'widgetHandler' && n.language === 'typescript')).toBe(true);
  126. });
  127. it('does NOT index the same file without codegraph.json (zero-config preserved)', async () => {
  128. write('widget.foo', SOURCE);
  129. const { nodes, files } = await indexAndQuery();
  130. expect(files.length).toBe(0);
  131. expect(nodes.length).toBe(0);
  132. });
  133. });
  134. });