mirror of
https://github.com/jsdoc/jsdoc.git
synced 2025-12-08 19:46:11 +00:00
410 lines
12 KiB
JavaScript
410 lines
12 KiB
JavaScript
/*
|
|
Copyright 2011 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.
|
|
*/
|
|
|
|
/* eslint-disable no-script-url */
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
import { Syntax, Walker } from '@jsdoc/ast';
|
|
import { combineDoclets, Doclet } from '@jsdoc/doclet';
|
|
|
|
import { attachTo } from '../../../lib/handlers.js';
|
|
import * as jsdocParser from '../../../lib/parser.js';
|
|
import { Visitor } from '../../../lib/visitor.js';
|
|
|
|
describe('@jsdoc/parse/lib/parser', () => {
|
|
const dirname = path.resolve(path.join(jsdoc.dirname(import.meta.url), '..', '..', '..'));
|
|
|
|
it('is an object', () => {
|
|
expect(jsdocParser).toBeObject();
|
|
});
|
|
|
|
it('exports a `createParser` method', () => {
|
|
expect(jsdocParser.createParser).toBeFunction();
|
|
});
|
|
|
|
it('exports a `Parser` constructor', () => {
|
|
expect(jsdocParser.Parser).toBeFunction();
|
|
});
|
|
|
|
describe('createParser', () => {
|
|
it('returns a `Parser` when called with dependencies', () => {
|
|
const parser = jsdocParser.createParser(jsdoc.deps);
|
|
|
|
expect(parser).toBeObject();
|
|
|
|
parser._stopListening();
|
|
});
|
|
});
|
|
|
|
describe('Parser', () => {
|
|
let parser;
|
|
|
|
beforeEach(() => {
|
|
parser = new jsdocParser.Parser(jsdoc.deps);
|
|
});
|
|
|
|
afterEach(() => {
|
|
parser._stopListening();
|
|
});
|
|
|
|
it('has a `visitor` property', () => {
|
|
expect(parser.visitor).toBeObject();
|
|
});
|
|
|
|
it('has a `walker` property', () => {
|
|
expect(parser.walker).toBeObject();
|
|
});
|
|
|
|
it('has a `parse` method', () => {
|
|
expect(parser.parse).toBeFunction();
|
|
});
|
|
|
|
it('has a `results` method', () => {
|
|
expect(parser.results).toBeFunction();
|
|
});
|
|
|
|
it('has an `addAstNodeVisitor` method', () => {
|
|
expect(parser.addAstNodeVisitor).toBeFunction();
|
|
});
|
|
|
|
it('has a `getAstNodeVisitors` method', () => {
|
|
expect(parser.getAstNodeVisitors).toBeFunction();
|
|
});
|
|
|
|
describe('visitor', () => {
|
|
it('contains an appropriate visitor by default', () => {
|
|
expect(parser.visitor instanceof Visitor).toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('walker', () => {
|
|
it('contains an appropriate walker by default', () => {
|
|
expect(parser.walker instanceof Walker).toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('parse', () => {
|
|
it('emits `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('allows `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('emits `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('emits `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('emits `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('allows `newDoclet` handlers to modify doclets', () => {
|
|
let doclets;
|
|
let docletStore;
|
|
const sourceCode = 'javascript:/** @class */function Foo() {}';
|
|
|
|
function handler(e) {
|
|
e.doclet = combineDoclets(e.doclet, new Doclet('', {}, jsdoc.deps));
|
|
e.doclet.foo = 'bar';
|
|
}
|
|
|
|
attachTo(parser);
|
|
docletStore = parser.on('newDoclet', handler).parse(sourceCode);
|
|
doclets = Array.from(docletStore.doclets).filter((d) => d.foo === 'bar');
|
|
|
|
expect(doclets).toBeArrayOfSize(1);
|
|
});
|
|
|
|
it('calls AST node visitors', () => {
|
|
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).toBeArrayOfSize(4);
|
|
|
|
// args[0]: AST node
|
|
expect(args[0].type).toBe(Syntax.VariableDeclarator);
|
|
|
|
// args[1]: JSDoc event
|
|
expect(args[1]).toBeObject();
|
|
expect(args[1].code).toBeObject();
|
|
expect(args[1].code.name).toBe('foo');
|
|
|
|
// args[2]: parser
|
|
expect(args[2]).toBeObject();
|
|
expect(args[2] instanceof jsdocParser.Parser).toBeTrue();
|
|
|
|
// args[3]: current source name
|
|
expect(String(args[3])).toBe('[[string0]]');
|
|
});
|
|
|
|
it('reflects 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).toBeObject();
|
|
expect(doclet.name).toBe('bar');
|
|
});
|
|
|
|
it('emits `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(eventObject.sourcefiles).toEqual(['[[string0]]']);
|
|
|
|
expect(eventObject.doclets).toBeArrayOfSize(1);
|
|
expect(eventObject.doclets[0].kind).toBe('class');
|
|
expect(eventObject.doclets[0].longname).toBe('Foo');
|
|
});
|
|
|
|
it('emits a `processingComplete` event when fireProcessingComplete is called', () => {
|
|
const spy = jasmine.createSpy();
|
|
const doclets = ['a', 'b'];
|
|
let mostRecentArg0;
|
|
|
|
parser.on('processingComplete', spy).fireProcessingComplete(doclets);
|
|
|
|
expect(spy).toHaveBeenCalled();
|
|
|
|
mostRecentArg0 = spy.calls.mostRecent().args[0];
|
|
expect(mostRecentArg0).toBeObject();
|
|
expect(mostRecentArg0.doclets).toBe(doclets);
|
|
});
|
|
|
|
it('does 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('can parse its own source file', () => {
|
|
const parserSrc = `javascript:${fs.readFileSync(
|
|
path.join(dirname, 'lib/parser.js'),
|
|
'utf8'
|
|
)}`;
|
|
|
|
function parse() {
|
|
parser.parse(parserSrc);
|
|
}
|
|
|
|
expect(parse).not.toThrow();
|
|
});
|
|
|
|
it('comments 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', () => {
|
|
it('returns an empty array before files are parsed', () => {
|
|
const results = parser.results();
|
|
|
|
expect(results).toBeEmptyArray();
|
|
});
|
|
|
|
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).toBeArrayOfSize(1);
|
|
expect(results[0]).toBeObject();
|
|
expect(results[0].name).toBe('foo');
|
|
});
|
|
|
|
it('reflects 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('emits interleaved `jsdocCommentFound` and `symbolFound` events, in source order', () => {
|
|
attachTo(parser);
|
|
parser.parse(source);
|
|
events.all
|
|
.slice()
|
|
.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;
|
|
|
|
it('works with a single node visitor', () => {
|
|
parser.addAstNodeVisitor(visitorA);
|
|
|
|
visitors = parser.getAstNodeVisitors();
|
|
|
|
expect(visitors).toEqual([visitorA]);
|
|
});
|
|
|
|
it('works with multiple node visitors', () => {
|
|
parser.addAstNodeVisitor(visitorA);
|
|
parser.addAstNodeVisitor(visitorB);
|
|
|
|
visitors = parser.getAstNodeVisitors();
|
|
|
|
expect(visitors).toEqual([visitorA, visitorB]);
|
|
});
|
|
});
|
|
|
|
describe('getAstNodeVisitors', () => {
|
|
it('returns an empty array by default', () => {
|
|
const visitors = parser.getAstNodeVisitors();
|
|
|
|
expect(visitors).toBeEmptyArray();
|
|
});
|
|
|
|
// other functionality is covered by the addNodeVisitors tests
|
|
});
|
|
});
|
|
});
|