/* Copyright 2020 the JSDoc Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ describe('@jsdoc/parse/lib/ast-node', () => { const astNode = require('../../../lib/ast-node'); const babelParser = require('@babel/parser'); const { parserOptions } = require('../../../lib/ast-builder'); const { Syntax } = require('../../../lib/syntax'); function parse(str) { return babelParser.parse(str, parserOptions).program.body[0]; } // create the AST nodes we'll be testing const arrayExpression = parse('[,]').expression; const arrowFunctionExpression = parse('var foo = () => {};').declarations[0].init; const assignmentExpression = parse('foo = 1;').expression; const binaryExpression = parse('foo & foo;').expression; const experimentalObjectRestSpread = parse('var one = {...two, three: 4};').declarations[0].init; const functionDeclaration1 = parse('function foo() {}'); const functionDeclaration2 = parse('function foo(bar) {}'); const functionDeclaration3 = parse('function foo(bar, baz, qux) {}'); const functionDeclaration4 = parse('function foo(...bar) {}'); const functionExpression1 = parse('var foo = function() {};').declarations[0].init; const functionExpression2 = parse('var foo = function(bar) {};').declarations[0].init; const identifier = parse('foo;').expression; const literal = parse('1;').expression; const memberExpression = parse('foo.bar;').expression; const memberExpressionComputed1 = parse('foo["bar"];').expression; const memberExpressionComputed2 = parse("foo['bar'];").expression; const methodDefinition1 = parse('class Foo { bar() {} }').body.body[0]; const methodDefinition2 = parse('var foo = () => class { bar() {} };').declarations[0].init.body .body[0]; const propertyGet = parse('var foo = { get bar() {} };').declarations[0].init.properties[0]; const propertyInit = parse('var foo = { bar: {} };').declarations[0].init.properties[0]; const propertySet = parse('var foo = { set bar(a) {} };').declarations[0].init.properties[0]; const thisExpression = parse('this;').expression; const unaryExpression1 = parse('+1;').expression; const unaryExpression2 = parse('+foo;').expression; const variableDeclarator1 = parse('var foo = 1;').declarations[0]; const variableDeclarator2 = parse('var foo;').declarations[0]; it('should exist', () => { expect(astNode).toBeObject(); }); it('should export an addNodeProperties method', () => { expect(astNode.addNodeProperties).toBeFunction(); }); it('should export a getInfo method', () => { expect(astNode.getInfo).toBeFunction(); }); it('should export a getParamNames method', () => { expect(astNode.getParamNames).toBeFunction(); }); it('should export an isAccessor method', () => { expect(astNode.isAccessor).toBeFunction(); }); it('should export an isAssignment method', () => { expect(astNode.isAssignment).toBeFunction(); }); it('should export an isFunction method', () => { expect(astNode.isFunction).toBeFunction(); }); it('should export an isScope method', () => { expect(astNode.isScope).toBeFunction(); }); it('should export a nodeToString method', () => { expect(astNode.nodeToString).toBeFunction(); }); it('should export a nodeToValue method', () => { expect(astNode.nodeToValue).toBeFunction(); }); describe('addNodeProperties', () => { it('should return null for undefined input', () => { expect(astNode.addNodeProperties()).toBe(null); }); it('should return null if the input is not an object', () => { expect(astNode.addNodeProperties('foo')).toBe(null); }); it('should preserve existing properties that are not "node properties"', () => { const node = astNode.addNodeProperties({ foo: 1 }); expect(node).toBeObject(); expect(node.foo).toBe(1); }); it('should add a nodeId if necessary', () => { const node = astNode.addNodeProperties({}); const descriptor = Object.getOwnPropertyDescriptor(node, 'nodeId'); expect(descriptor).toBeObject(); expect(descriptor.value).toBeString(); expect(descriptor.enumerable).toBeTrue(); }); it('should not overwrite an existing nodeId', () => { const nodeId = 'foo'; const node = astNode.addNodeProperties({ nodeId: nodeId }); expect(node.nodeId).toBe(nodeId); }); it('should add a non-enumerable, writable parent if necessary', () => { const node = astNode.addNodeProperties({}); const descriptor = Object.getOwnPropertyDescriptor(node, 'parent'); expect(descriptor).toBeDefined(); expect(descriptor.value).toBeUndefined(); expect(descriptor.enumerable).toBeFalse(); expect(descriptor.writable).toBeTrue(); }); it('should not overwrite an existing parent', () => { const parent = {}; const node = astNode.addNodeProperties({ parent: parent }); expect(node.parent).toBe(parent); }); it('should not overwrite a null parent', () => { const node = astNode.addNodeProperties({ parent: null }); expect(node.parent).toBeNull(); }); it('should add an enumerable parentId', () => { const node = astNode.addNodeProperties({}); const descriptor = Object.getOwnPropertyDescriptor(node, 'parentId'); expect(descriptor).toBeObject(); expect(descriptor.enumerable).toBeTrue(); }); it('should provide a null parentId for nodes with no parent', () => { const node = astNode.addNodeProperties({}); expect(node.parentId).toBeNull(); }); it('should provide a non-null parentId for nodes with a parent', () => { const node = astNode.addNodeProperties({}); const parent = astNode.addNodeProperties({}); node.parent = parent; expect(node.parentId).toBe(parent.nodeId); }); it('should add a non-enumerable, writable enclosingScope if necessary', () => { const node = astNode.addNodeProperties({}); const descriptor = Object.getOwnPropertyDescriptor(node, 'enclosingScope'); expect(descriptor).toBeObject(); expect(descriptor.value).toBeUndefined(); expect(descriptor.enumerable).toBeFalse(); expect(descriptor.writable).toBeTrue(); }); it('should not overwrite an existing enclosingScope', () => { const enclosingScope = {}; const node = astNode.addNodeProperties({ enclosingScope: enclosingScope }); expect(node.enclosingScope).toBe(enclosingScope); }); it('should not overwrite a null enclosingScope', () => { const node = astNode.addNodeProperties({ enclosingScope: null }); expect(node.enclosingScope).toBeNull(); }); it('should add an enumerable enclosingScopeId', () => { const node = astNode.addNodeProperties({}); const descriptor = Object.getOwnPropertyDescriptor(node, 'enclosingScopeId'); expect(descriptor).toBeObject(); expect(descriptor.enumerable).toBeTrue(); }); it('should provide a null enclosingScopeId for nodes with no enclosing scope', () => { const node = astNode.addNodeProperties({}); expect(node.enclosingScopeId).toBeNull(); }); it('should provide a non-null enclosingScopeId for nodes with an enclosing scope', () => { const node = astNode.addNodeProperties({}); const enclosingScope = astNode.addNodeProperties({}); node.enclosingScope = enclosingScope; expect(node.enclosingScopeId).toBe(enclosingScope.nodeId); }); }); describe('getInfo', () => { it('should throw an error for undefined input', () => { function noNode() { astNode.getInfo(); } expect(noNode).toThrow(); }); it('should return the correct info for an AssignmentExpression', () => { const info = astNode.getInfo(assignmentExpression); expect(info).toBeObject(); expect(info.node).toBeObject(); expect(info.node.type).toBe(Syntax.Literal); expect(info.node.value).toBe(1); expect(info.name).toBe('foo'); expect(info.type).toBe(Syntax.Literal); expect(info.value).toBe(1); }); it('should return the correct info for a FunctionDeclaration', () => { const info = astNode.getInfo(functionDeclaration2); expect(info).toBeObject(); expect(info.node).toBeObject(); expect(info.node.type).toBe(Syntax.FunctionDeclaration); expect(info.name).toBe('foo'); expect(info.type).toBe(Syntax.FunctionDeclaration); expect(info.value).toBeUndefined(); expect(info.paramnames).toBeArrayOfSize(1); expect(info.paramnames[0]).toBe('bar'); }); it('should return the correct info for a FunctionExpression', () => { const info = astNode.getInfo(functionExpression2); expect(info).toBeObject(); expect(info.node).toBeObject(); expect(info.node.type).toBe(Syntax.FunctionExpression); expect(info.name).toBe(''); expect(info.type).toBe(Syntax.FunctionExpression); expect(info.value).toBeUndefined(); expect(info.paramnames).toBeArrayOfSize(1); expect(info.paramnames[0]).toBe('bar'); }); it('should return the correct info for a MemberExpression', () => { const info = astNode.getInfo(memberExpression); expect(info).toBeObject(); expect(info.node).toBeObject(); expect(info.node.type).toBe(Syntax.MemberExpression); expect(info.name).toBe('foo.bar'); expect(info.type).toBe(Syntax.MemberExpression); }); it('should return the correct info for a computed MemberExpression', () => { const info = astNode.getInfo(memberExpressionComputed1); expect(info).toBeObject(); expect(info.node).toBeObject(); expect(info.node.type).toBe(Syntax.MemberExpression); expect(info.name).toBe('foo["bar"]'); expect(info.type).toBe(Syntax.MemberExpression); }); it('should return the correct info for a Property initializer', () => { const info = astNode.getInfo(propertyInit); expect(info).toBeObject(); expect(info.node).toBeObject(); expect(info.node.type).toBe(Syntax.ObjectExpression); expect(info.name).toBe('bar'); expect(info.type).toBe(Syntax.ObjectExpression); }); it('should return the correct info for a Property setter', () => { const info = astNode.getInfo(propertySet); expect(info).toBeObject(); expect(info.node).toBeObject(); expect(info.node.type).toBe(Syntax.FunctionExpression); expect(info.name).toBe('bar'); expect(info.type).toBeUndefined(); expect(info.value).toBeUndefined(); expect(info.paramnames).toBeArrayOfSize(1); expect(info.paramnames[0]).toBe('a'); }); it('should return the correct info for a VariableDeclarator with a value', () => { const info = astNode.getInfo(variableDeclarator1); expect(info).toBeObject(); expect(info.node).toBeObject(); expect(info.node.type).toBe(Syntax.Literal); expect(info.name).toBe('foo'); expect(info.type).toBe(Syntax.Literal); expect(info.value).toBe(1); }); it('should return the correct info for a VariableDeclarator with no value', () => { const info = astNode.getInfo(variableDeclarator2); expect(info).toBeObject(); expect(info.node).toBeObject(); expect(info.node.type).toBe(Syntax.Identifier); expect(info.name).toBe('foo'); expect(info.type).toBeUndefined(); expect(info.value).toBeUndefined(); }); it('should return the correct info for other node types', () => { const info = astNode.getInfo(binaryExpression); expect(info).toBeObject(); expect(info.node).toBe(binaryExpression); expect(info.type).toBe(Syntax.BinaryExpression); }); }); describe('getParamNames', () => { it('should return an empty array for undefined input', () => { const params = astNode.getParamNames(); expect(params).toBeEmptyArray(); }); it('should return an empty array if the input has no params property', () => { const params = astNode.getParamNames({}); expect(params).toBeEmptyArray(); }); it('should return an empty array if the input has no params', () => { const params = astNode.getParamNames(functionDeclaration1); expect(params).toBeEmptyArray(); }); it('should return a single-item array if the input has a single param', () => { const params = astNode.getParamNames(functionDeclaration2); expect(params).toEqual(['bar']); }); it('should return a multi-item array if the input has multiple params', () => { const params = astNode.getParamNames(functionDeclaration3); expect(params).toEqual(['bar', 'baz', 'qux']); }); it('should include rest parameters', () => { const params = astNode.getParamNames(functionDeclaration4); expect(params).toEqual(['bar']); }); }); describe('isAccessor', () => { it('should return false for undefined values', () => { expect(astNode.isAccessor()).toBeFalse(); }); it('should return false if the parameter is not an object', () => { expect(astNode.isAccessor('foo')).toBeFalse(); }); it('should return false for non-Property nodes', () => { expect(astNode.isAccessor(binaryExpression)).toBeFalse(); }); it('should return false for Property nodes whose kind is "init"', () => { expect(astNode.isAccessor(propertyInit)).toBeFalse(); }); it('should return true for Property nodes whose kind is "get"', () => { expect(astNode.isAccessor(propertyGet)).toBeTrue(); }); it('should return true for Property nodes whose kind is "set"', () => { expect(astNode.isAccessor(propertySet)).toBeTrue(); }); }); describe('isAssignment', () => { it('should return false for undefined values', () => { expect(astNode.isAssignment()).toBeFalse(); }); it('should return false if the parameter is not an object', () => { expect(astNode.isAssignment('foo')).toBeFalse(); }); it('should return false for nodes that are not assignments', () => { expect(astNode.isAssignment(binaryExpression)).toBeFalse(); }); it('should return true for AssignmentExpression nodes', () => { expect(astNode.isAssignment(assignmentExpression)).toBeTrue(); }); it('should return true for VariableDeclarator nodes', () => { expect(astNode.isAssignment(variableDeclarator1)).toBeTrue(); }); }); describe('isFunction', () => { it('should recognize function declarations as functions', () => { expect(astNode.isFunction(functionDeclaration1)).toBeTrue(); }); it('should recognize function expressions as functions', () => { expect(astNode.isFunction(functionExpression1)).toBeTrue(); }); it('should recognize method definitions as functions', () => { expect(astNode.isFunction(methodDefinition1)).toBeTrue(); }); it('should recognize arrow function expressions as functions', () => { expect(astNode.isFunction(arrowFunctionExpression)).toBeTrue(); }); it('should recognize non-functions', () => { expect(astNode.isFunction(arrayExpression)).toBeFalse(); }); }); describe('isScope', () => { it('should return false for undefined values', () => { expect(astNode.isScope()).toBeFalse(); }); it('should return false if the parameter is not an object', () => { expect(astNode.isScope('foo')).toBeFalse(); }); it('should return true for CatchClause nodes', () => { expect(astNode.isScope({ type: Syntax.CatchClause })).toBeTrue(); }); it('should return true for FunctionDeclaration nodes', () => { expect(astNode.isScope({ type: Syntax.FunctionDeclaration })).toBeTrue(); }); it('should return true for FunctionExpression nodes', () => { expect(astNode.isScope({ type: Syntax.FunctionExpression })).toBeTrue(); }); it('should return false for other nodes', () => { expect(astNode.isScope({ type: Syntax.NameExpression })).toBeFalse(); }); }); describe('nodeToString', () => { it('should be an alias to nodeToValue', () => { expect(astNode.nodeToString).toBe(astNode.nodeToValue); }); }); describe('nodeToValue', () => { it('should return `[null]` for the sparse array `[,]`', () => { expect(astNode.nodeToValue(arrayExpression)).toBe('[null]'); }); it('should return the variable name for assignment expressions', () => { expect(astNode.nodeToValue(assignmentExpression)).toBe('foo'); }); it('should return the function name for function declarations', () => { expect(astNode.nodeToValue(functionDeclaration1)).toBe('foo'); }); it('should return undefined for anonymous function expressions', () => { expect(astNode.nodeToValue(functionExpression1)).toBeUndefined(); }); it('should return the identifier name for identifiers', () => { expect(astNode.nodeToValue(identifier)).toBe('foo'); }); it('should return the literal value for literals', () => { expect(astNode.nodeToValue(literal)).toBe(1); }); it('should return the object and property for noncomputed member expressions', () => { expect(astNode.nodeToValue(memberExpression)).toBe('foo.bar'); }); it( 'should return the object and property, with a computed property that uses the same ' + 'quote character as the original source, for computed member expressions', () => { expect(astNode.nodeToValue(memberExpressionComputed1)).toBe('foo["bar"]'); expect(astNode.nodeToValue(memberExpressionComputed2)).toBe("foo['bar']"); } ); // TODO: we can't test this here because JSDoc, not Babylon, adds the `parent` property to // nodes. also, we currently return an empty string instead of `` in this case; // see `module:@jsdoc/parse.astNode.nodeToValue` and the comment on // `Syntax.MethodDefinition` for details xit( 'should return `` for method definitions inside classes that were ' + 'returned by an arrow function expression', () => { expect(astNode.nodeToValue(methodDefinition2)).toBe(''); } ); it('should return "this" for this expressions', () => { expect(astNode.nodeToValue(thisExpression)).toBe('this'); }); it('should return the operator and nodeToValue value for prefix unary expressions', () => { expect(astNode.nodeToValue(unaryExpression1)).toBe('+1'); expect(astNode.nodeToValue(unaryExpression2)).toBe('+foo'); }); it('should throw an error for postfix unary expressions', () => { function postfixNodeToValue() { // there's no valid source representation for this one, so we fake it const unaryExpressionPostfix = (() => { const node = parse('+1;').body[0].expression; node.prefix = false; return node; })(); return astNode.nodeToValue(unaryExpressionPostfix); } expect(postfixNodeToValue).toThrow(); }); it('should return the variable name for variable declarators', () => { expect(astNode.nodeToValue(variableDeclarator1)).toBe('foo'); }); it('should return an empty string for all other nodes', () => { expect(astNode.nodeToValue(binaryExpression)).toBe(''); }); it('should understand and ignore ExperimentalSpreadProperty', () => { expect(astNode.nodeToValue(experimentalObjectRestSpread)).toBe('{"three":4}'); }); }); });