strip-cstyle-differential.test.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. import { describe, it, expect } from 'vitest';
  2. import { stripCommentsForRegex } from '../src/resolution/strip-comments';
  3. import { getKernel } from '../src/extraction/kernel/loader';
  4. /**
  5. * The pre-optimization split('')-based stripCStyle, kept verbatim as the
  6. * ORACLE: the rewritten segment-builder must be byte-identical on every
  7. * input (the C fn-pointer synthesizer's regexes run over this text, and any
  8. * divergence would silently change synthesized edges).
  9. */
  10. function referenceStripCStyle(src: string, allowSingleQuoteStrings: boolean): string {
  11. const out = src.split('');
  12. let i = 0;
  13. const n = src.length;
  14. const blankRange = (buf: string[], start: number, end: number): void => {
  15. for (let k = start; k < end; k++) {
  16. buf[k] = src[k] === '\n' ? '\n' : ' ';
  17. }
  18. };
  19. while (i < n) {
  20. const c = src[i]!;
  21. const c2 = src[i + 1] ?? '';
  22. if (c === '/' && c2 === '*') {
  23. const start = i;
  24. i += 2;
  25. while (i < n && !(src[i] === '*' && src[i + 1] === '/')) i++;
  26. if (i < n) i += 2;
  27. blankRange(out, start, i);
  28. continue;
  29. }
  30. if (c === '/' && c2 === '/') {
  31. const start = i;
  32. while (i < n && src[i] !== '\n') i++;
  33. blankRange(out, start, i);
  34. continue;
  35. }
  36. if (c === '"' || (allowSingleQuoteStrings && c === "'") || c === '`') {
  37. const quote = c;
  38. i++;
  39. while (i < n && src[i] !== quote) {
  40. if (src[i] === '\\' && i + 1 < n) {
  41. i += 2;
  42. continue;
  43. }
  44. if (quote !== '`' && src[i] === '\n') break;
  45. i++;
  46. }
  47. if (i < n && src[i] === quote) i++;
  48. continue;
  49. }
  50. i++;
  51. }
  52. return out.join('');
  53. }
  54. const FIXTURES: Array<[string, string]> = [
  55. ['plain code, no comments', 'int main(void) {\n\treturn a / b;\n}\n'],
  56. ['block comment', 'int x; /* a comment\nspanning lines */ int y;\n'],
  57. ['line comment', 'int x; // trailing\nint y;\n'],
  58. ['comment markers inside string', 'const char *s = "/* not a comment */ // nor this";\nint z;\n'],
  59. ['string inside comment', '/* "a string" inside */ int q;\n'],
  60. ['unterminated block comment', 'int a;\n/* runs to the end'],
  61. ['unterminated string', 'const char *s = "no close\nint b; /* real comment */\n'],
  62. ['escape at end of string', 'const char *s = "ends with backslash \\\\";\nint c;\n'],
  63. ['escape as last char of file', 'const char *s = "\\'],
  64. ['star at last char', 'int d; /*'],
  65. ['slash at last char', 'int e; /'],
  66. ['crlf line comment', 'int f; // comment\r\nint g;\r\n'],
  67. ['unicode in comment', 'int h; /* café résumé — dash */\nint i;\n'],
  68. ['astral chars in comment', 'int j; /* 🚀🎉 emoji */\nint k;\n'],
  69. ['unicode in string', 'const char *s = "café 🚀";\nint l;\n'],
  70. ['nested-looking block', '/* outer /* inner */ int m;\n'],
  71. ['comment right after string', '"str"/*c*/int n;\n'],
  72. ['backtick template (js mode relevance)', 'const t = `multi\nline ${x} // not comment`;\nint o;\n'],
  73. ['single quotes with escapes', "char c = '\\''; // char literal\nint p;\n"],
  74. ['empty input', ''],
  75. ['only a newline', '\n'],
  76. ['only a comment', '/*x*/'],
  77. ];
  78. describe('stripCStyle segment-builder vs split-based oracle', () => {
  79. for (const [name, src] of FIXTURES) {
  80. it(`fixture: ${name} (c mode)`, () => {
  81. expect(stripCommentsForRegex(src, 'c')).toBe(referenceStripCStyle(src, false));
  82. });
  83. it(`fixture: ${name} (js mode, single-quote strings on)`, () => {
  84. expect(stripCommentsForRegex(src, 'javascript')).toBe(referenceStripCStyle(src, true));
  85. });
  86. }
  87. it('randomized differential (seeded, 500 cases)', () => {
  88. // Tiny deterministic LCG — no Math.random in tests that must reproduce.
  89. let seed = 0x2fn;
  90. const rand = (max: number): number => {
  91. seed = (seed * 6364136223846793005n + 1442695040888963407n) & 0xffffffffffffffffn;
  92. return Number(seed % BigInt(max));
  93. };
  94. const ATOMS = ['/*', '*/', '//', '\n', '"', "'", '`', '\\', 'x', ' ', '/', '*', 'é', '🚀', '\r\n', 'int a;'];
  95. for (let caseN = 0; caseN < 500; caseN++) {
  96. let s = '';
  97. const len = rand(40);
  98. for (let k = 0; k < len; k++) s += ATOMS[rand(ATOMS.length)]!;
  99. expect(stripCommentsForRegex(s, 'c'), `c-mode case ${caseN}: ${JSON.stringify(s)}`).toBe(
  100. referenceStripCStyle(s, false)
  101. );
  102. expect(stripCommentsForRegex(s, 'javascript'), `js-mode case ${caseN}: ${JSON.stringify(s)}`).toBe(
  103. referenceStripCStyle(s, true)
  104. );
  105. }
  106. });
  107. it('comment-free input returns the identical string (zero-copy path)', () => {
  108. const src = 'static int add(int a, int b) {\n\treturn a + b;\n}\n';
  109. expect(stripCommentsForRegex(src, 'c')).toBe(src);
  110. });
  111. });
  112. // The native kernel's C stripper (codegraph-kernel/src/cfnptr.rs) blanks per
  113. // UTF-16 code unit precisely so its output is string-identical to the TS
  114. // stripper — the cFnPtr extraction sweep's scanners then run over the same
  115. // character stream on both paths. Pinned here against the same fixtures and
  116. // randomized corpus as the TS rewrite.
  117. const kernelStrip = getKernel()?.cfnptrStripC;
  118. describe.runIf(typeof kernelStrip === 'function')('native cfnptrStripC vs TS stripper (c mode)', () => {
  119. for (const [name, src] of FIXTURES) {
  120. it(`fixture: ${name}`, () => {
  121. expect(kernelStrip!(src)).toBe(stripCommentsForRegex(src, 'c'));
  122. });
  123. }
  124. it('randomized differential (seeded, 500 cases)', () => {
  125. let seed = 0x2fn;
  126. const rand = (max: number): number => {
  127. seed = (seed * 6364136223846793005n + 1442695040888963407n) & 0xffffffffffffffffn;
  128. return Number(seed % BigInt(max));
  129. };
  130. const ATOMS = ['/*', '*/', '//', '\n', '"', "'", '`', '\\', 'x', ' ', '/', '*', 'é', '🚀', '\r\n', 'int a;'];
  131. for (let caseN = 0; caseN < 500; caseN++) {
  132. let s = '';
  133. const len = rand(40);
  134. for (let k = 0; k < len; k++) s += ATOMS[rand(ATOMS.length)]!;
  135. expect(kernelStrip!(s), `case ${caseN}: ${JSON.stringify(s)}`).toBe(stripCommentsForRegex(s, 'c'));
  136. }
  137. });
  138. });