const { assert, test, skip, module: describe, only } = require('qunit'); const sinon = require('sinon'); const acorn = require('acorn'); const { FunctionTracer } = require('../../src'); describe('internal: FunctionTracer'); test('works with Program', () => { const ast = acorn.parse(`var i;`); const functionTracer = new FunctionTracer(ast); assert.ok(functionTracer.functionContexts.length > 0); }); test('works with BlockStatement', () => { const mockBody = {}; let called = false; let calledBody = null; const mockInstance = { contexts: [], runningContexts: [], newContext: FunctionTracer.prototype.newContext, scan: (body) => { called = true; calledBody = body; } }; FunctionTracer.prototype.scan.call(mockInstance, { type: 'BlockStatement', body: mockBody }); assert.ok(called); assert.equal(calledBody, mockBody); assert.equal(mockInstance.contexts.length, 1); }); test('works with AssignmentExpression', () => { const mockLeft = {}; const mockRight = {}; let called = false; let calledSides = []; const mockInstance = { scan: (side) => { called = true; calledSides.push(side); } }; FunctionTracer.prototype.scan.call(mockInstance, { type: 'AssignmentExpression', left: mockLeft, right: mockRight }); assert.ok(called); assert.deepEqual(calledSides, [mockLeft, mockRight]); }); test('works with LogicalExpression', () => { const mockLeft = {}; const mockRight = {}; let called = false; let calledSides = []; const mockInstance = { scan: (side) => { called = true; calledSides.push(side); } }; FunctionTracer.prototype.scan.call(mockInstance, { type: 'LogicalExpression', left: mockLeft, right: mockRight }); assert.ok(called); assert.deepEqual(calledSides, [mockLeft, mockRight]); }); test('works with BinaryExpression', () => { const mockLeft = {}; const mockRight = {}; let called = false; let calledSides = []; const mockInstance = { scan: (side) => { called = true; calledSides.push(side); } }; FunctionTracer.prototype.scan.call(mockInstance, { type: 'BinaryExpression', left: mockLeft, right: mockRight }); assert.ok(called); assert.deepEqual(calledSides, [mockLeft, mockRight]); }); test('works with UpdateExpression', () => { const mockArgument = {}; let called = false; let calledBody = null; const mockInstance = { scan: (argument) => { called = true; calledBody = argument; } }; FunctionTracer.prototype.scan.call(mockInstance, { type: 'UpdateExpression', argument: mockArgument }); assert.ok(called); assert.equal(calledBody, mockArgument); }); test('works with UnaryExpression', () => { const mockArgument = {}; let called = false; let calledArgument = null; const mockInstance = { scan: (argument) => { called = true; calledArgument = argument; } }; FunctionTracer.prototype.scan.call(mockInstance, { type: 'UpdateExpression', argument: mockArgument }); assert.ok(called); assert.equal(calledArgument, mockArgument); }); test('works with VariableDeclaration', () => { const mockDeclarations = []; let called = false; let calledDeclarations = null; const mockInstance = { scan: (declarations) => { called = true; calledDeclarations = declarations; } }; FunctionTracer.prototype.scan.call(mockInstance, { type: 'VariableDeclaration', declarations: mockDeclarations }); assert.ok(called); assert.deepEqual(calledDeclarations, mockDeclarations); }); test('works with generic VariableDeclarator', () => { const ast = acorn.parse('var bob = 0;'); const functionTracer = new FunctionTracer(ast); const { bob } = functionTracer.contexts[0]; assert.equal(bob.ast, ast.body[0].declarations[0]); assert.equal(bob.context, functionTracer.contexts[0]); assert.equal(bob.name, 'bob'); assert.equal(bob.origin, 'declaration'); assert.equal(bob.assignable, true); assert.equal(bob.inForLoopTest, null); assert.equal(bob.inForLoopInit, false); assert.equal(functionTracer.declarations[0], bob); }); test('works with var VariableDeclarator', () => { const ast = acorn.parse('var bob = 0;'); const functionTracer = new FunctionTracer(ast); const { bob } = functionTracer.contexts[0]; assert.equal(bob.context['@contextType'], 'function'); }); test('works with let VariableDeclarator', () => { const ast = acorn.parse('let bob = 0;'); const functionTracer = new FunctionTracer(ast); const { bob } = functionTracer.contexts[0]; assert.equal(bob.context['@contextType'], 'function'); }); test('works with var & let VariableDeclarator together', () => { const ast = acorn.parse(`var bob = 0; for (let i = 0; i < 1; i++) { let pop = 0; }`); const functionTracer = new FunctionTracer(ast); assert.equal(functionTracer.contexts[0].bob.context['@contextType'], 'function'); assert.equal(functionTracer.contexts[0].i, undefined); assert.equal(functionTracer.contexts[0].pop, undefined); assert.equal(functionTracer.contexts[1].bob.context['@contextType'], 'function'); assert.equal(functionTracer.contexts[1].i.context['@contextType'], 'function'); assert.equal(functionTracer.contexts[1].pop, undefined); assert.equal(functionTracer.contexts[2].bob.context['@contextType'], 'function'); assert.equal(functionTracer.contexts[2].i.context['@contextType'], 'function'); assert.equal(functionTracer.contexts[2].pop, undefined); assert.equal(functionTracer.contexts[3].bob.context['@contextType'], 'function'); assert.equal(functionTracer.contexts[3].i.context['@contextType'], 'function'); assert.equal(functionTracer.contexts[3].pop.context['@contextType'], 'function'); }); test('works with FunctionExpression when runningContexts.length = 0', () => { const mockBody = {}; let called = false; let calledBody = null; const mockInstance = { runningContexts: [], functions: [], scan: (body) => { called = true; calledBody = body; } }; FunctionTracer.prototype.scan.call(mockInstance, { type: 'FunctionExpression', body: mockBody }); assert.ok(called); assert.equal(calledBody, mockBody); assert.equal(mockInstance.functions.length, 0); }); test('works with FunctionDeclaration when runningContexts.length = 0', () => { const mockBody = {}; let called = false; let calledBody = null; const mockInstance = { runningContexts: [], functions: [], scan: (body) => { called = true; calledBody = body; } }; FunctionTracer.prototype.scan.call(mockInstance, { type: 'FunctionDeclaration', body: mockBody }); assert.ok(called); assert.equal(calledBody, mockBody); assert.equal(mockInstance.functions.length, 0); }); test('works with FunctionExpression when runningContexts.length > 0', () => { const mockBody = {}; const mockInstance = { functions: [], runningContexts: [null], scan: () => { throw new Error('should not be called'); } }; const mockAst = { type: 'FunctionExpression', body: mockBody }; FunctionTracer.prototype.scan.call(mockInstance, mockAst); assert.equal(mockInstance.functions.length, 1); assert.equal(mockInstance.functions[0], mockAst); }); test('works with FunctionDeclaration when runningContexts.length > 0', () => { const mockBody = {}; const mockInstance = { functions: [], runningContexts: [null], scan: () => { throw new Error('should not be called'); } }; const mockAst = { type: 'FunctionDeclaration', body: mockBody }; FunctionTracer.prototype.scan.call(mockInstance, mockAst); assert.equal(mockInstance.functions.length, 1); assert.equal(mockInstance.functions[0], mockAst); }); test('works with IfStatement', () => { const mockTest = {}; const mockConsequent = {}; const mockAlternate = {}; let called = false; let calledArgs = []; const mockInstance = { scan: (arg) => { called = true; calledArgs.push(arg); } }; FunctionTracer.prototype.scan.call(mockInstance, { type: 'IfStatement', test: mockTest, consequent: mockConsequent, alternate: mockAlternate, }); assert.ok(called); assert.deepEqual(calledArgs, [mockTest, mockConsequent, mockAlternate]); }); test('works with ForStatement', () => { const ast = acorn.parse(`for (let i = 0; i < 1; i++) { call(); }`); const functionTracer = new FunctionTracer(ast.body[0]); assert.equal(functionTracer.declarations[0].name, 'i'); assert.equal(functionTracer.contexts.length, 4); }); test('works with DoWhileStatement', () => { const mockBody = {}; const mockTest = {}; let called = false; let calledArgs = []; const mockInstance = { contexts: [], runningContexts: [], newContext: FunctionTracer.prototype.newContext, scan: (arg) => { called = true; calledArgs.push(arg); } }; FunctionTracer.prototype.scan.call(mockInstance, { type: 'DoWhileStatement', body: mockBody, test: mockTest }); assert.ok(called); assert.deepEqual(calledArgs, [mockBody, mockTest]); }); test('works with WhileStatement', () => { const mockBody = {}; const mockTest = {}; let called = false; let calledArgs = []; const mockInstance = { contexts: [], runningContexts: [], newContext: FunctionTracer.prototype.newContext, scan: (arg) => { called = true; calledArgs.push(arg); } }; FunctionTracer.prototype.scan.call(mockInstance, { type: 'WhileStatement', body: mockBody, test: mockTest }); assert.ok(called); assert.deepEqual(calledArgs, [mockBody, mockTest]); }); test('works with Identifier', () => { const mockCurrentContext = {}; const mockIsState = sinon.spy(); const mockGetDeclaration = sinon.spy(() => 123); const mockInstance = { identifiers: [], currentContext: mockCurrentContext, isState: mockIsState, getDeclaration: mockGetDeclaration, }; const mockAst = { type: 'Identifier', name: 'x' }; FunctionTracer.prototype.scan.call(mockInstance, mockAst); assert.ok(mockGetDeclaration.called); assert.equal(mockGetDeclaration.args[0][0], 'x'); assert.deepEqual(mockInstance.identifiers, [ { context: mockInstance.currentContext, ast: mockAst, declaration: 123 } ]); assert.equal(mockIsState.args[0][0], 'trackIdentifiers'); }); test('works with ReturnStatement', () => { const mockArgument = {}; let called = false; let calledArgument = null; const mockInstance = { returnStatements: [], scan: (argument) => { called = true; calledArgument = argument; } }; const mockAst = { type: 'ReturnStatement', argument: mockArgument }; FunctionTracer.prototype.scan.call(mockInstance, mockAst); assert.ok(called); assert.equal(calledArgument, mockArgument); assert.equal(mockInstance.returnStatements[0], mockAst); }); test('works with MemberExpression', () => { const mockBody = {}; const mockProperty = {}; const mockPushState = sinon.spy(); const mockPopState = sinon.spy(); const mockScan = sinon.spy(); const mockInstance = { scan: mockScan, pushState: mockPushState, popState: mockPopState, }; FunctionTracer.prototype.scan.call(mockInstance, { type: 'MemberExpression', object: mockBody, property: mockProperty }); assert.ok(mockScan.called); assert.equal(mockScan.args[0][0], mockBody); assert.equal(mockScan.args[1][0], mockProperty); assert.equal(mockPushState.args[0][0], 'memberExpression'); assert.equal(mockPopState.args[0][0], 'memberExpression'); }); test('works with ExpressionStatement', () => { const mockExpression = {}; let called = false; let calledExpression = null; const mockInstance = { scan: (body) => { called = true; calledExpression = body; } }; FunctionTracer.prototype.scan.call(mockInstance, { type: 'ExpressionStatement', expression: mockExpression }); assert.ok(called); assert.equal(calledExpression, mockExpression); }); test('works with SequenceExpression', () => { const mockExpression = {}; const mockExpressions = [mockExpression]; let called = false; let calledExpression = null; const mockInstance = { scan: (body) => { called = true; calledExpression = body; } }; FunctionTracer.prototype.scan.call(mockInstance, { type: 'SequenceExpression', expressions: mockExpressions }); assert.ok(called); assert.equal(calledExpression, mockExpressions); }); test('works with CallExpression', () => { const mockArguments = {}; let called = false; let calledArguments = null; const mockCurrentContext = {}; const mockInstance = { currentContext: mockCurrentContext, functionCalls: [], scan: (_arguments) => { called = true; calledArguments = _arguments; } }; const mockAst = { type: 'CallExpression', arguments: mockArguments }; FunctionTracer.prototype.scan.call(mockInstance, mockAst); assert.ok(called); assert.equal(calledArguments, mockArguments); assert.deepEqual(mockInstance.functionCalls, [ { context: mockCurrentContext, ast: mockAst } ]); }); test('works with ArrayExpression', () => { const mockElements = {}; let called = false; let calledElements = null; const mockInstance = { scan: (elements) => { called = true; calledElements = elements; } }; FunctionTracer.prototype.scan.call(mockInstance, { type: 'ArrayExpression', elements: mockElements }); assert.ok(called); assert.equal(calledElements, mockElements); }); test('works with ConditionalExpression', () => { const mockTest = {}; const mockAlternate = {}; const mockConsequent = {}; let called = false; let calledArgs = []; const mockInstance = { scan: (arg) => { called = true; calledArgs.push(arg); } }; FunctionTracer.prototype.scan.call(mockInstance, { type: 'ConditionalExpression', test: mockTest, alternate: mockAlternate, consequent: mockConsequent }); assert.ok(called); assert.deepEqual(calledArgs, [mockTest, mockConsequent, mockConsequent]); }); test('works with SwitchStatement', () => { const mockDiscriminant = {}; const mockCases = {}; let called = false; let calledArgs = []; const mockInstance = { scan: (arg) => { called = true; calledArgs.push(arg); } }; FunctionTracer.prototype.scan.call(mockInstance, { type: 'SwitchStatement', discriminant: mockDiscriminant, cases: mockCases }); assert.ok(called); assert.deepEqual(calledArgs, [mockDiscriminant, mockCases]); }); test('works with SwitchCase', () => { const mockTest = {}; const mockConsequent = {}; let called = false; let calledArgs = []; const mockInstance = { scan: (arg) => { called = true; calledArgs.push(arg); } }; FunctionTracer.prototype.scan.call(mockInstance, { type: 'SwitchCase', test: mockTest, consequent: mockConsequent }); assert.ok(called); assert.deepEqual(calledArgs, [mockTest, mockConsequent]); }); test('does nothing with un-scan-ables', () => { let called = false; const mockInstance = { scan: () => { called = true; } }; [ 'ThisExpression', 'Literal', 'DebuggerStatement', 'EmptyStatement', 'BreakStatement', 'ContinueStatement' ].forEach(type => { FunctionTracer.prototype.scan.call(mockInstance, { type }); }); assert.ok(!called); }); test('when called with fake type, throws', () => { assert.throws(() => { FunctionTracer.prototype.scan.call({}, { type: 'Made Up' }); }); }); test('can handle direct arrays', () => { const mockBlockBody = {}; const mockProgramBody = {}; const asts = [ { type: 'BlockStatement' }, { type: 'Program' }, ]; const calledAsts = []; const mockInstance = { scan: (ast) => { calledAsts.push(ast); } }; FunctionTracer.prototype.scan.call(mockInstance, asts); assert.deepEqual(calledAsts, asts); });