jsinspect-plus
Version:
Detect copy-pasted and structurally similar code. Supports ES2020 standard (and most proposed features), TS and TSX files. Using Babel 8's parser.
260 lines (230 loc) • 8.43 kB
JavaScript
const expect = require('expect.js');
const fixtures = require('./fixtures');
const helpers = require('./helpers');
const NodeUtils = require('../lib/nodeUtils');
describe('NodeUtils', function() {
describe('walkSubtrees', function() {
it('walks each child node using DFS', function() {
const res = [];
const root = helpers.parse(fixtures.simple)[0];
NodeUtils.walkSubtrees(root, node => res.push(node.type));
expect(res).to.eql([
'ExpressionStatement',
'UpdateExpression',
'Identifier',
'ExpressionStatement',
'CallExpression',
'Identifier'
]);
});
});
describe('getDFSTraversal', function() {
it('returns the DFS traversal of the AST', function() {
const ast = helpers.parse(fixtures.simple)[0];
const res = NodeUtils.getDFSTraversal(ast).map(node => node.type);
expect(res).to.eql([
'BlockStatement',
'ExpressionStatement',
'UpdateExpression',
'Identifier',
'ExpressionStatement',
'CallExpression',
'Identifier'
]);
});
});
describe('getBFSTraversal', function() {
it('returns the BFS traversal of the AST', function() {
const ast = helpers.parse(fixtures.simple)[0];
const res = NodeUtils.getBFSTraversal(ast).map(node => node.type);
expect(res).to.eql([
'BlockStatement',
'ExpressionStatement',
'ExpressionStatement',
'UpdateExpression',
'CallExpression',
'Identifier',
'Identifier'
]);
});
});
describe('getChildren', function() {
it('returns the children of a Node', function() {
const parent = helpers.parse(fixtures.intersection)[0];
const res = NodeUtils.getChildren(parent);
// Children should be the three identifiers intersectionA,
// array1, and array2, followed by a block statement
expect(res.map(node => node.type)).to.eql([
'Identifier',
'Identifier',
'Identifier',
'BlockStatement'
])
});
it('ignores null children', function() {
const parent = helpers.parse(fixtures.nullChildren)[1].expression.left;
// parent.elements is an array with a leading null that should be ignored,
// followed by an identifier
const res = NodeUtils.getChildren(parent);
expect(res).to.have.length(1);
expect(res[0].type).to.be('Identifier');
});
it('ignores empty JSXText nodes', function() {
const parent = helpers.parse(fixtures.jsxNesting)[0].expression;
const res = NodeUtils.getChildren(parent);
res.forEach(node => expect(node.type).not.to.be('JSXText'));
});
});
describe('isBefore', function() {
describe('given nodes with different line numbers', function() {
it('returns true if the first node has a lower line number', function() {
const res = NodeUtils.isBefore(
{loc: {start: {line: 0, column: 0}}},
{loc: {start: {line: 1, column: 0}}}
);
expect(res).to.be(true);
});
it('returns false if the first node has a higher numbered line', function() {
const res = NodeUtils.isBefore(
{loc: {start: {line: 1, column: 0}}},
{loc: {start: {line: 0, column: 0}}}
);
expect(res).to.be(false);
});
});
describe('given nodes with the same line number', function() {
it('returns true if the first node has a lower column number', function() {
const res = NodeUtils.isBefore(
{loc: {start: {line: 0, column: 0}}},
{loc: {start: {line: 0, column: 1}}}
);
expect(res).to.be(true);
});
it('returns false if the first node has a higher column number', function() {
const res = NodeUtils.isBefore(
{loc: {start: {line: 0, column: 1}}},
{loc: {start: {line: 0, column: 0}}}
);
expect(res).to.be(false);
});
});
});
describe('isES6ModuleImport', function() {
it('returns true for an import declaration', function() {
// ImportDeclaration
const nodes = [helpers.parse(fixtures.es6Module)[0]];
expect(NodeUtils.isES6ModuleImport(nodes)).to.be(true);
});
it('returns false for export declaration', function() {
// ExportNamedDeclaration
const nodes = [helpers.parse(fixtures.es6Module)[1]];
expect(NodeUtils.isES6ModuleImport(nodes)).to.be(false);
});
it('returns false otherwise', function() {
const nodes = helpers.parse(fixtures.commonjs);
expect(NodeUtils.isES6ModuleImport(nodes)).to.be(false);
});
});
describe('isAMD', function() {
it('returns true for an expression containing a define', function() {
// First expression is define
const nodes = [helpers.parse(fixtures.amd)[0]];
expect(NodeUtils.isAMD(nodes)).to.be(true);
});
it('returns true for an expression containing a define', function() {
// Third expression is require
const nodes = [helpers.parse(fixtures.amd)[2]];
expect(NodeUtils.isAMD(nodes)).to.be(true);
});
it('returns true even if the function is a property', function() {
const nodes = [helpers.parse(fixtures.amd)[4]];
expect(NodeUtils.isAMD(nodes)).to.be(true);
});
it('returns true even if a nested property', function() {
const nodes = [helpers.parse(fixtures.amd)[6]];
expect(NodeUtils.isAMD(nodes)).to.be(true);
});
it('returns false otherwise', function() {
const nodes = helpers.parse(fixtures.commonjs);
expect(NodeUtils.isAMD(nodes)).to.be(false);
});
});
describe('isCommonJS', function() {
it('returns true for an expression containing a require', function() {
// First node is an ExpressionStatement
const nodes = [helpers.parse(fixtures.commonjs)[0]];
expect(NodeUtils.isCommonJS(nodes)).to.be(true);
});
it('returns true for a declaration containing a require', function() {
// Second node is a constiableDeclaration
const nodes = [helpers.parse(fixtures.commonjs)[1]];
expect(NodeUtils.isCommonJS(nodes)).to.be(true);
});
it('returns false otherwise', function() {
const nodes = helpers.parse(fixtures.amd);
expect(NodeUtils.isCommonJS(nodes)).to.be(false);
});
});
describe('typesMatch', function() {
it('returns true if all node types match', function() {
const res = NodeUtils.typesMatch([
{type: 'a'}, {type: 'a'}, {type: 'a'}
]);
expect(res).to.be(true);
});
it('returns false if not all node types match', function() {
const res = NodeUtils.typesMatch([
{type: 'a'}, {type: 'a'}, {type: 'b'}
]);
expect(res).to.be(false);
});
});
describe('identifiersMatch', function() {
it('returns true if all node are matching identifiers', function() {
const res = NodeUtils.identifiersMatch([
{name: 'a'}, {name: 'a'}, {name: 'a'}
]);
expect(res).to.be(true);
});
it('returns false if not all node names match', function() {
const res = NodeUtils.identifiersMatch([
{name: 'a'}, {name: 'a'}, {name: 'b'}
]);
expect(res).to.be(false);
});
});
describe('literalsMatch', function() {
it('returns true if all literals have the same value', function() {
const res = NodeUtils.literalsMatch([
{type: 'Literal', value: 'a'},
{type: 'Literal', value: 'a'},
{type: 'Literal', value: 'a'}
]);
expect(res).to.be(true);
});
it('returns false if not all literals have the same value', function() {
const res = NodeUtils.literalsMatch([
{type: 'Literal', value: 'a'},
{type: 'Literal', value: 'a'},
{type: 'Literal', value: 'b'}
]);
expect(res).to.be(false);
});
it('treats JSXText as a literal', function() {
const res = NodeUtils.literalsMatch([
{type: 'JSXText', value: 'a'},
{type: 'JSXText', value: 'a'},
{type: 'JSXText', value: 'b'}
]);
expect(res).to.be(false);
});
it('ignores the values of nodes which are not literals', function() {
const res = NodeUtils.literalsMatch([
{type: 'Foo', value: 'a'},
{type: 'Foo', value: 'a'},
{type: 'Foo', value: 'b'}
]);
expect(res).to.be(true);
});
});
});