jsdoc/test/specs/jsdoc/src/parser.js
Jeff Williams 617b3236bf Replace old, vendored Jasmine with current npm package.
JSDoc-specific test functions are now properties of a `jsdoc` global, not a `jasmine` global.

Also updates license files to reflect the fact that we no longer vendor anything.
2019-05-12 15:10:38 -07:00

446 lines
16 KiB
JavaScript

/* eslint-disable no-script-url */
describe('jsdoc/src/parser', () => {
const { attachTo } = require('jsdoc/src/handlers');
const { dirname } = require('jsdoc/env');
const fs = require('jsdoc/fs');
const jsdocParser = require('jsdoc/src/parser');
const logger = require('jsdoc/util/logger');
const path = require('jsdoc/path');
it('should exist', () => {
expect(jsdocParser).toBeDefined();
expect(typeof jsdocParser).toBe('object');
});
it('should export a "createParser" method', () => {
expect(typeof jsdocParser.createParser).toBe('function');
});
it('should export a "Parser" constructor', () => {
expect(typeof jsdocParser.Parser).toBe('function');
});
describe('createParser', () => {
it('should return a Parser when called without arguments', () => {
expect(typeof jsdocParser.createParser()).toBe('object');
});
it('should create a jsdoc/src/parser.Parser instance with the argument "js"', () => {
const parser = jsdocParser.createParser('js');
expect(parser instanceof jsdocParser.Parser).toBe(true);
});
it('should log a fatal error on bad input', () => {
spyOn(logger, 'fatal');
jsdocParser.createParser('not-a-real-parser-ever');
expect(logger.fatal).toHaveBeenCalled();
});
});
describe('Parser', () => {
let parser;
function newParser() {
parser = new jsdocParser.Parser();
}
newParser();
it('should have an "astBuilder" property', () => {
expect(parser.astBuilder).toBeDefined();
});
it('should have a "visitor" property', () => {
expect(parser.visitor).toBeDefined();
});
it('should have a "walker" property', () => {
expect(parser.walker).toBeDefined();
});
it('should accept an astBuilder, visitor, and walker as arguments', () => {
const astBuilder = {};
const visitor = {
/* eslint-disable no-empty-function */
setParser() {}
/* eslint-enable no-empty-function */
};
const walker = {};
const myParser = new jsdocParser.Parser(astBuilder, visitor, walker);
expect(myParser.astBuilder).toBe(astBuilder);
expect(myParser.visitor).toBe(visitor);
expect(myParser.walker).toBe(walker);
});
it('should have a "parse" method', () => {
expect(parser.parse).toBeDefined();
expect(typeof parser.parse).toBe('function');
});
it('should have a "results" method', () => {
expect(parser.results).toBeDefined();
expect(typeof parser.results).toBe('function');
});
it('should have an "addAstNodeVisitor" method', () => {
expect(parser.addAstNodeVisitor).toBeDefined();
expect(typeof parser.addAstNodeVisitor).toBe('function');
});
it('should have a "getAstNodeVisitors" method', () => {
expect(parser.getAstNodeVisitors).toBeDefined();
expect(typeof parser.getAstNodeVisitors).toBe('function');
});
describe('astBuilder', () => {
it('should contain an appropriate astBuilder by default', () => {
expect(parser.astBuilder instanceof (require('jsdoc/src/astbuilder')).AstBuilder).toBe(true);
});
});
describe('visitor', () => {
it('should contain an appropriate visitor by default', () => {
expect(parser.visitor instanceof (require('jsdoc/src/visitor')).Visitor).toBe(true);
});
});
describe('walker', () => {
it('should contain an appropriate walker by default', () => {
expect(parser.walker instanceof (require('jsdoc/src/walker')).Walker).toBe(true);
});
});
describe('parse', () => {
beforeEach(newParser);
it('should fire "parseBegin" events before it parses any files', () => {
const spy = jasmine.createSpy();
const sourceFiles = ['javascript:/** @name foo */'];
parser.on('parseBegin', spy).parse(sourceFiles);
expect(spy).toHaveBeenCalled();
expect(spy.calls.mostRecent().args[0].sourcefiles).toBe(sourceFiles);
});
it("should allow 'parseBegin' handlers to modify the list of source files", () => {
const sourceCode = 'javascript:/** @name foo */';
const newFiles = ['[[replaced]]'];
let evt;
function handler(e) {
e.sourcefiles = newFiles;
evt = e;
}
parser.on('parseBegin', handler).parse(sourceCode);
expect(evt.sourcefiles).toBe(newFiles);
});
it('should fire "jsdocCommentFound" events when a source file contains JSDoc comments', () => {
const spy = jasmine.createSpy();
const sourceCode = ['javascript:/** @name bar */'];
parser.on('jsdocCommentFound', spy).parse(sourceCode);
expect(spy).toHaveBeenCalled();
expect(spy.calls.mostRecent().args[0].comment).toBe('/** @name bar */');
});
it('should fire "symbolFound" events when a source file contains named symbols', () => {
const spy = jasmine.createSpy();
const sourceCode = 'javascript:var foo = 1';
parser.on('symbolFound', spy).parse(sourceCode);
expect(spy).toHaveBeenCalled();
});
it('should fire "newDoclet" events after creating a new doclet', () => {
const spy = jasmine.createSpy();
const sourceCode = 'javascript:var foo = 1';
parser.on('symbolFound', spy).parse(sourceCode);
expect(spy).toHaveBeenCalled();
});
it('should allow "newDoclet" handlers to modify doclets', () => {
let results;
const sourceCode = 'javascript:/** @class */function Foo() {}';
function handler(e) {
const doop = require('jsdoc/util/doop');
e.doclet = doop(e.doclet);
e.doclet.foo = 'bar';
}
attachTo(parser);
parser.on('newDoclet', handler).parse(sourceCode);
results = parser.results();
expect(results[0].foo).toBe('bar');
});
it('should call AST node visitors', () => {
const Syntax = require('jsdoc/src/syntax').Syntax;
let args;
const sourceCode = ['javascript:/** foo */var foo;'];
const visitor = {
visitNode(node, e) {
if (e && e.code && !args) {
args = Array.prototype.slice.call(arguments);
}
}
};
attachTo(parser);
parser.addAstNodeVisitor(visitor);
parser.parse(sourceCode);
expect(args).toBeDefined();
expect( Array.isArray(args) ).toBe(true);
expect(args.length).toBe(4);
// args[0]: AST node
expect(args[0].type).toBeDefined();
expect(args[0].type).toBe(Syntax.VariableDeclarator);
// args[1]: JSDoc event
expect(typeof args[1]).toBe('object');
expect(args[1].code).toBeDefined();
expect(args[1].code.name).toBeDefined();
expect(args[1].code.name).toBe('foo');
// args[2]: parser
expect(typeof args[2]).toBe('object');
expect(args[2] instanceof jsdocParser.Parser).toBe(true);
// args[3]: current source name
expect( String(args[3]) ).toBe('[[string0]]');
});
it('should reflect changes made by AST node visitors', () => {
let doclet;
const sourceCode = ['javascript:/** foo */var foo;'];
const visitor = {
visitNode(node, e) {
if (e && e.code && e.code.name === 'foo') {
e.code.name = 'bar';
}
}
};
attachTo(parser);
parser.addAstNodeVisitor(visitor);
parser.parse(sourceCode);
doclet = parser.results()[0];
expect(doclet).toBeDefined();
expect(typeof doclet).toBe('object');
expect(doclet.name).toBeDefined();
expect(doclet.name).toBe('bar');
});
it('should fire "parseComplete" events after it finishes parsing files', () => {
let eventObject;
const spy = jasmine.createSpy();
const sourceCode = ['javascript:/** @class */function Foo() {}'];
attachTo(parser);
parser.on('parseComplete', spy).parse(sourceCode);
expect(spy).toHaveBeenCalled();
eventObject = spy.calls.mostRecent().args[0];
expect(eventObject).toBeDefined();
expect( Array.isArray(eventObject.sourcefiles) ).toBe(true);
expect(eventObject.sourcefiles.length).toBe(1);
expect(eventObject.sourcefiles[0]).toBe('[[string0]]');
expect( Array.isArray(eventObject.doclets) ).toBe(true);
expect(eventObject.doclets.length).toBe(1);
expect(eventObject.doclets[0].kind).toBe('class');
expect(eventObject.doclets[0].longname).toBe('Foo');
});
it('should fire a "processingComplete" event when fireProcessingComplete is called', () => {
const spy = jasmine.createSpy();
const doclets = ['a', 'b'];
parser.on('processingComplete', spy).fireProcessingComplete(doclets);
expect(spy).toHaveBeenCalled();
expect(typeof spy.calls.mostRecent().args[0]).toBe('object');
expect(spy.calls.mostRecent().args[0].doclets).toBeDefined();
expect(spy.calls.mostRecent().args[0].doclets).toBe(doclets);
});
it('should not throw errors when parsing files with ES6 syntax', () => {
function parse() {
const parserSrc = `javascript:${fs.readFileSync(
path.join(dirname, 'test/fixtures/es6.js'), 'utf8')}`;
parser.parse(parserSrc);
}
expect(parse).not.toThrow();
});
it('should be able to parse its own source file', () => {
const parserSrc = `javascript:${fs.readFileSync(path.join(dirname,
'lib/jsdoc/src/parser.js'), 'utf8')}`;
function parse() {
parser.parse(parserSrc);
}
expect(parse).not.toThrow();
});
it('should comment out a POSIX hashbang at the start of the file', () => {
const parserSrc = 'javascript:#!/usr/bin/env node\n/** class */function Foo() {}';
function parse() {
parser.parse(parserSrc);
}
expect(parse).not.toThrow();
});
});
describe('results', () => {
beforeEach(newParser);
it('returns an empty array before files are parsed', () => {
const results = parser.results();
expect(results).toBeDefined();
expect( Array.isArray(results) ).toBe(true);
expect(results.length).toBe(0);
});
it('returns an array of doclets after files are parsed', () => {
const source = 'javascript:var foo;';
let results;
attachTo(parser);
parser.parse(source);
results = parser.results();
expect(results).toBeDefined();
expect(results[0]).toBeDefined();
expect(typeof results[0]).toBe('object');
expect(results[0].name).toBeDefined();
expect(results[0].name).toBe('foo');
});
it('should reflect comment changes made by "jsdocCommentFound" handlers', () => {
// we test both POSIX and Windows line endings
const source = 'javascript:/**\n * replaceme\r\n * @module foo\n */\n\n' +
'/**\n * replaceme\n */\nvar bar;';
parser.on('jsdocCommentFound', e => {
e.comment = e.comment.replace('replaceme', 'REPLACED!');
});
attachTo(parser);
parser.parse(source);
parser.results().forEach(({comment}) => {
expect(comment).not.toMatch('replaceme');
expect(comment).toMatch('REPLACED!');
});
});
// TODO: this test appears to be doing nothing...
xdescribe('event order', () => {
const events = {
all: [],
jsdocCommentFound: [],
symbolFound: []
};
const source = fs.readFileSync(path.join(dirname,
'test/fixtures/eventorder.js'), 'utf8');
/*
function pushEvent(e) {
events.all.push(e);
events[e.event].push(e);
}
*/
function sourceOrderSort(atom1, atom2) {
if (atom1.range[1] < atom2.range[0]) {
return -1;
}
else if (atom1.range[0] < atom2.range[0] && atom1.range[1] === atom2.range[1]) {
return 1;
}
else {
return 0;
}
}
it('should fire interleaved jsdocCommentFound and symbolFound events, ' +
'in source order', () => {
attachTo(parser);
parser.parse(source);
events.all.slice(0).sort(sourceOrderSort).forEach((e, i) => {
expect(e).toBe(events.all[i]);
});
});
});
});
describe('addAstNodeVisitor', () => {
/* eslint-disable no-empty-function */
function visitorA() {}
function visitorB() {}
/* eslint-enable no-empty-function */
let visitors;
beforeEach(newParser);
it('should work with a single node visitor', () => {
parser.addAstNodeVisitor(visitorA);
visitors = parser.getAstNodeVisitors();
expect(visitors.length).toBe(1);
expect(visitors[0]).toBe(visitorA);
});
it('should work with multiple node visitors', () => {
parser.addAstNodeVisitor(visitorA);
parser.addAstNodeVisitor(visitorB);
visitors = parser.getAstNodeVisitors();
expect(visitors.length).toBe(2);
expect(visitors[0]).toBe(visitorA);
expect(visitors[1]).toBe(visitorB);
});
});
describe('getAstNodeVisitors', () => {
beforeEach(newParser);
it('should return an empty array by default', () => {
const visitors = parser.getAstNodeVisitors();
expect( Array.isArray(visitors) ).toBe(true);
expect(visitors.length).toBe(0);
});
// other functionality is covered by the addNodeVisitors tests
});
});
});