Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions __tests__/commonjs-eval-facts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { DatabaseConnection } from '../src/db';
import { QueryBuilder } from '../src/db/queries';
import type { FileRecord } from '../src/types';

describe('stored CommonJS eval facts', () => {
let root: string;
let db: DatabaseConnection;
let queries: QueryBuilder;
const file = (contentHash: string, value?: boolean | null): FileRecord => ({
path: 'm.js', contentHash, language: 'javascript', size: 1,
modifiedAt: 1, indexedAt: 1, nodeCount: 0, commonJsEvalCall: value,
});
beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-eval-facts-'));
db = DatabaseConnection.initialize(path.join(root, 'graph.db'));
queries = new QueryBuilder(db.getDb());
});
afterEach(() => { db.close(); fs.rmSync(root, { recursive: true, force: true }); });

it('does not prove absence from missing, malformed, or inconclusive facts', () => {
expect(queries.getCommonJsEvalCall('m.js', 'v1')).toBe(null);
queries.upsertFile(file('v1', null));
expect(queries.getCommonJsEvalCall('m.js', 'v1')).toBe(null);
queries.setMetadata('commonjs_eval:m.js', '{broken');
expect(queries.getCommonJsEvalCall('m.js', 'v1')).toBe(null);
});

it('requires an exact source hash and replaces the verdict on a new store', () => {
queries.upsertFile(file('v1', false));
expect(queries.getCommonJsEvalCall('m.js', 'v1')).toBe(false);
expect(queries.getCommonJsEvalCall('m.js', 'v2')).toBe(null);
queries.upsertFile(file('v2', true));
expect(queries.getCommonJsEvalCall('m.js', 'v1')).toBe(null);
expect(queries.getCommonJsEvalCall('m.js', 'v2')).toBe(true);
});

it('rolls back the fact with the surrounding file-store transaction', () => {
queries.upsertFile(file('v1', false));
expect(() => queries.runInTransaction(() => {
queries.upsertFile(file('v2', true));
throw new Error('store failed');
})).toThrow('store failed');
expect(queries.getFileByPath('m.js')!.contentHash).toBe('v1');
expect(queries.getCommonJsEvalCall('m.js', 'v1')).toBe(false);
});

it('removes facts with their file instead of leaking them to a replacement', () => {
queries.upsertFile(file('v1', true));
queries.deleteFile('m.js');
expect(queries.getCommonJsEvalCall('m.js', 'v1')).toBe(null);
queries.upsertFile(file('v1'));
expect(queries.getCommonJsEvalCall('m.js', 'v1')).toBe(null);
});
});
67 changes: 67 additions & 0 deletions __tests__/commonjs-eval.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { beforeAll, describe, expect, it } from 'vitest';
import { getParser, loadGrammarsForLanguages } from '../src/extraction/grammars';
import { hasCommonJsEvalCall } from '../src/extraction/commonjs-eval';
import type { Language } from '../src/types';

beforeAll(async () => { await loadGrammarsForLanguages(['javascript', 'typescript', 'tsx']); });

function read(source: string, language: Language = 'javascript'): boolean | null {
const tree = getParser(language)!.parse(source)!;
try { return hasCommonJsEvalCall(tree.rootNode); }
finally { tree.delete(); }
}

describe('AST recognition of CommonJS eval calls', () => {
it.each([
"eval('exports.load = Store.prototype.load;');",
"((eval))(('exports.load = Store.prototype.load;'));",
'eval(`exports.load = Store.prototype.load;`);',
'eval(`exports.load = ${code};`);',
"if (true) (eval)('exports.load = Store.prototype.load;');",
"for (let i = 0; i < 1; i++) ((eval))('exports.load = Store.prototype.load;');",
"function setup() { return (eval)('exports.load = Store.prototype.load;'); }",
"function* setup() { yield (eval)('exports.load = Store.prototype.load;'); }",
"const setup = { *run() { yield (eval)('exports.load = Store.prototype.load;'); } };",
"function* setup() { const obj = { [yield (eval)('exports.load = Store.prototype.load;')]() {} }; }",
"function yield(value) {} yield(eval('exports.load = Store.prototype.load;'));",
'const text = `${eval(code)}`;',
'eval(code);',
])('finds a real callee: %s', (source) => { expect(read(source)).toBe(true); });

it.each([
'wrap(eval)("ignored");',
'wrap((eval))(("ignored"));',
'(makeWrap())(eval)("ignored");',
'wrap["call"](eval)("ignored");',
'wrap?.(eval)("ignored");',
'wrap`tag`(eval)("ignored");',
'if (eval) ("ignored");',
'function await(value) { return function ignore(code) {}; } await(eval)("ignored");',
'function yield(value) { return function ignore(code) {}; } yield(eval)("ignored");',
'function outer() { function yield(value) {} return yield(eval)("ignored"); }',
'function* outer() { function nested() { function yield(value) {} yield(eval)("ignored"); } }',
'function yield(value) {} const obj = { *[yield(eval)("ignored")]() {} };',
'class Evaluator { eval(code) {} }',
'const text = "eval(code)";',
'const text = `eval(code)`;',
'/* eval(code) */',
'const pattern = /eval(code)/;',
'holder.eval(code);',
])('does not confuse another expression with the callee: %s', (source) => {
expect(read(source)).toBe(false);
});

it.each(['typescript', 'tsx'] as const)('uses the existing %s grammar', (language) => {
expect(read("if (true) (eval)('exports.load = Store.prototype.load;');", language)).toBe(true);
expect(read('wrap(eval)("ignored");', language)).toBe(false);
});

it('keeps a real call in a JSX expression', () => {
expect(read('<div title="eval(code)">{(eval)(code)}</div>', 'tsx')).toBe(true);
expect(read('<div title="eval(code)">eval(code)</div>', 'tsx')).toBe(false);
});

it('does not prove absence from malformed source', () => {
expect(read('function broken( {')).toBe(null);
});
});
Loading
Loading