Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@microsoft/tsdoc-config",
"comment": "Add support for configuring synonyms.",
"type": "minor"
}
],
"packageName": "@microsoft/tsdoc-config",
"email": "ron.buckton@microsoft.com"
}
11 changes: 11 additions & 0 deletions common/changes/@microsoft/tsdoc/synonyms_2019-12-19-22-47.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@microsoft/tsdoc",
"comment": "Add support for synonyms and the 'see' tag.",
"type": "minor"
}
],
"packageName": "@microsoft/tsdoc",
"email": "ron.buckton@microsoft.com"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "eslint-plugin-tsdoc",
"comment": "",
"type": "none"
}
],
"packageName": "eslint-plugin-tsdoc",
"email": "ron.buckton@microsoft.com"
}
56 changes: 55 additions & 1 deletion tsdoc-config/src/TSDocConfigFile.ts
Original file line number Diff line number Diff line change
@@ -32,13 +32,24 @@ interface ITagConfigJson {
tagName: string;
syntaxKind: 'inline' | 'block' | 'modifier';
allowMultiple?: boolean;
synonyms?: string[];
}

interface ISynonymConfigJson {
add?: ISynonymSetJson;
remove?: ISynonymSetJson;
}

interface ISynonymSetJson {
[tagName: string]: string[];
}

interface IConfigJson {
$schema: string;
tsdocVersion: string;
extends?: string[];
tagDefinitions: ITagConfigJson[];
synonyms?: ISynonymConfigJson;
}

/**
@@ -64,6 +75,8 @@ export class TSDocConfigFile {
private _tsdocSchema: string;
private readonly _extendsPaths: string[];
private readonly _tagDefinitions: TSDocTagDefinition[];
private readonly _synonymAdditions: Map<string, string[]>;
private readonly _synonymDeletions: Map<string, string[]>;

private constructor() {
this.log = new ParserMessageLog();
@@ -75,7 +88,9 @@ export class TSDocConfigFile {
this._fileMTime = 0;
this._tsdocSchema = '';
this._extendsPaths = [];
this._tagDefinitions= [];
this._tagDefinitions = [];
this._synonymAdditions = new Map<string, string[]>();
this._synonymDeletions = new Map<string, string[]>();
}

/**
@@ -132,6 +147,14 @@ export class TSDocConfigFile {
return this._tagDefinitions;
}

public get synonymAdditions(): ReadonlyMap<string, ReadonlyArray<string>> {
return this._synonymAdditions;
}

public get synonymDeletions(): ReadonlyMap<string, ReadonlyArray<string>> {
return this._synonymDeletions;
}

/**
* This can be used for cache eviction. It returns true if the modification timestamp has changed for
* any of the files that were read when loading this `TSDocConfigFile`, which indicates that the file should be
@@ -227,9 +250,22 @@ export class TSDocConfigFile {
this._tagDefinitions.push(new TSDocTagDefinition({
tagName: jsonTagDefinition.tagName,
syntaxKind: syntaxKind,
synonyms: jsonTagDefinition.synonyms,
allowMultiple: jsonTagDefinition.allowMultiple
}));
}
if (configJson.synonyms) {
if (configJson.synonyms.add) {
for (const tagName of Object.keys(configJson.synonyms.add)) {
this._synonymAdditions.set(tagName, configJson.synonyms.add[tagName]);
}
}
if (configJson.synonyms.remove) {
for (const tagName of Object.keys(configJson.synonyms.remove)) {
this._synonymDeletions.set(tagName, configJson.synonyms.remove[tagName]);
}
}
}
}

private _loadWithExtends(configFilePath: string, referencingConfigFile: TSDocConfigFile | undefined,
@@ -388,5 +424,23 @@ export class TSDocConfigFile {
for (const tagDefinition of this.tagDefinitions) {
configuration.addTagDefinition(tagDefinition);
}

this.synonymDeletions.forEach((synonyms, tagName) => {
const tagDefinition: TSDocTagDefinition | undefined
= configuration.tryGetTagDefinition(tagName);
if (!tagDefinition) {
throw new Error(`A tag with the name ${tagName} could not be found.`);
}
configuration.removeSynonym(tagDefinition, ...synonyms);
});

this.synonymAdditions.forEach((synonyms, tagName) => {
const tagDefinition: TSDocTagDefinition | undefined
= configuration.tryGetTagDefinition(tagName);
if (!tagDefinition) {
throw new Error(`A tag with the name ${tagName} could not be found.`);
}
configuration.addSynonym(tagDefinition, ...synonyms);
});
}
}
72 changes: 72 additions & 0 deletions tsdoc-config/src/__tests__/TSDocConfigFile.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as path from 'path';

import { TSDocConfigFile } from '../TSDocConfigFile';
import { TSDocSynonymCollection } from '@microsoft/tsdoc/lib/configuration/TSDocSynonymCollection';

function getRelativePath(testPath: string): string {
return path
@@ -23,10 +24,32 @@ expect.addSnapshotSerializer({
extendsPaths: value.extendsPaths,
extendsFiles: value.extendsFiles,
tagDefinitions: value.tagDefinitions,
synonymAdditions: Array.from(value.synonymAdditions).reduce<Record<string, ReadonlyArray<string>>>(
(obj, [key, value]) => {
obj[key] = value;
return obj;
},
{}
),
synonymDeletions: Array.from(value.synonymDeletions).reduce<Record<string, ReadonlyArray<string>>>(
(obj, [key, value]) => {
obj[key] = value;
return obj;
},
{}
),
messages: value.log.messages
});
}
});
expect.addSnapshotSerializer({
test(value: unknown) {
return value instanceof TSDocSynonymCollection;
},
print(value: TSDocSynonymCollection, serialize: (value: unknown) => string, indent: (str: string) => string): string {
return serialize(value.synonyms);
}
});

function testLoadingFolder(assetPath: string): TSDocConfigFile {
return TSDocConfigFile.loadForFolder(path.join(__dirname, assetPath));
@@ -40,6 +63,8 @@ test('Load p1', () => {
"fileNotFound": false,
"filePath": "assets/p1/tsdoc.json",
"messages": Array [],
"synonymAdditions": Object {},
"synonymDeletions": Object {},
"tagDefinitions": Array [],
"tsdocSchema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
}
@@ -66,6 +91,8 @@ test('Load p2', () => {
"unformattedText": "File not found",
},
],
"synonymAdditions": Object {},
"synonymDeletions": Object {},
"tagDefinitions": Array [],
"tsdocSchema": "",
}
@@ -81,8 +108,11 @@ test('Load p3', () => {
"fileNotFound": false,
"filePath": "assets/p3/base1/tsdoc-base1.json",
"messages": Array [],
"synonymAdditions": Object {},
"synonymDeletions": Object {},
"tagDefinitions": Array [
TSDocTagDefinition {
"_synonymCollection": Array [],
"allowMultiple": false,
"standardization": "None",
"syntaxKind": 2,
@@ -98,8 +128,11 @@ test('Load p3', () => {
"fileNotFound": false,
"filePath": "assets/p3/base2/tsdoc-base2.json",
"messages": Array [],
"synonymAdditions": Object {},
"synonymDeletions": Object {},
"tagDefinitions": Array [
TSDocTagDefinition {
"_synonymCollection": Array [],
"allowMultiple": false,
"standardization": "None",
"syntaxKind": 2,
@@ -117,8 +150,11 @@ test('Load p3', () => {
"fileNotFound": false,
"filePath": "assets/p3/tsdoc.json",
"messages": Array [],
"synonymAdditions": Object {},
"synonymDeletions": Object {},
"tagDefinitions": Array [
TSDocTagDefinition {
"_synonymCollection": Array [],
"allowMultiple": false,
"standardization": "None",
"syntaxKind": 2,
@@ -140,8 +176,11 @@ test('Load p4', () => {
"fileNotFound": false,
"filePath": "assets/p4/node_modules/example-lib/dist/tsdoc-example.json",
"messages": Array [],
"synonymAdditions": Object {},
"synonymDeletions": Object {},
"tagDefinitions": Array [
TSDocTagDefinition {
"_synonymCollection": Array [],
"allowMultiple": false,
"standardization": "None",
"syntaxKind": 2,
@@ -158,8 +197,11 @@ test('Load p4', () => {
"fileNotFound": false,
"filePath": "assets/p4/tsdoc.json",
"messages": Array [],
"synonymAdditions": Object {},
"synonymDeletions": Object {},
"tagDefinitions": Array [
TSDocTagDefinition {
"_synonymCollection": Array [],
"allowMultiple": false,
"standardization": "None",
"syntaxKind": 2,
@@ -171,3 +213,33 @@ test('Load p4', () => {
}
`);
});
test('Load synonyms', () => {
expect(testLoadingFolder('assets/synonyms')).toMatchInlineSnapshot(`
Object {
"extendsFiles": Array [],
"extendsPaths": Array [],
"fileNotFound": false,
"filePath": "assets/synonyms/tsdoc.json",
"messages": Array [],
"synonymAdditions": Object {
"@readonly": Array [
"@readonly2",
],
},
"synonymDeletions": Object {},
"tagDefinitions": Array [
TSDocTagDefinition {
"_synonymCollection": Array [
"@bar",
],
"allowMultiple": false,
"standardization": "None",
"syntaxKind": 1,
"tagName": "@foo",
"tagNameWithUpperCase": "@FOO",
},
],
"tsdocSchema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
}
`);
});
2 changes: 2 additions & 0 deletions tsdoc-config/src/__tests__/assets/synonyms/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
}
11 changes: 11 additions & 0 deletions tsdoc-config/src/__tests__/assets/synonyms/tsdoc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
"tagDefinitions": [
{ "tagName": "@foo", "syntaxKind": "block", "synonyms": ["@bar"] }
],
"synonyms": {
"add": {
"@readonly": ["@readonly2"]
}
}
}
34 changes: 34 additions & 0 deletions tsdoc/schemas/tsdoc.schema.json
Original file line number Diff line number Diff line change
@@ -22,6 +22,22 @@
"items": {
"$ref": "#/definitions/tsdocTagDefinition"
}
},

"synonyms": {
"description": "Additional synonyms to add or remove from built-in tag definitions.",
"type": "object",
"properties": {
"add": {
"description": "Synonyms to add.",
"$ref": "#/definitions/synonymSet"
},
"remove": {
"description": "Synonyms to remove.",
"$ref": "#/definitions/synonymSet"
}
},
"additionalProperties": false
}
},
"required": [ "$schema" ],
@@ -44,10 +60,28 @@
"allowMultiple": {
"description": "If true, then this tag may appear multiple times in a doc comment. By default, a tag may only appear once.",
"type": "boolean"
},
"synonyms": {
"description": "Synonyms of the custom tag. TSDoc tag names start with an at-sign (@) followed by ASCII letters using camelCase capitalization.",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": ["tagName", "syntaxKind"],
"additionalProperties": false
},
"synonymSet": {
"description": "Provides the assocation between a tag and the synonyms to be added or removed.",
"type": "object",
"additionalProperties": {
"description": "Synonyms of the tag. TSDoc tag names start with an at-sign (@) followed by ASCII letters using camelCase capitalization.",
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
15 changes: 15 additions & 0 deletions tsdoc/src/__tests__/ParsingBasics.test.ts
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import {
TSDocTagSyntaxKind
} from '../index';
import { TestHelpers } from '../parser/__tests__/TestHelpers';
import { StandardTags } from '../details/StandardTags';

test('01 Simple @beta and @internal extraction', () => {
const parserContext: ParserContext = TestHelpers.parseAndMatchDocCommentSnapshot([
@@ -157,4 +158,18 @@ test('07 Invalid JSDoc type', () => {
' * @public',
' */'
].join('\n'));
});

test('08 synonyms', () => {
const configuration: TSDocConfiguration = new TSDocConfiguration();
configuration.addSynonym(StandardTags.readonly, "@readonly2");
TestHelpers.parseAndMatchDocCommentSnapshot([
'/**',
' * @param a - description1',
' * @arg b - description2',
' * @argument c - description3',
' * @return description4',
' * @readonly2',
' */'
].join('\n'), configuration);
});
273 changes: 273 additions & 0 deletions tsdoc/src/__tests__/__snapshots__/ParsingBasics.test.ts.snap
Original file line number Diff line number Diff line change
@@ -3002,3 +3002,276 @@ Object {
],
}
`;
exports[`08 synonyms 1`] = `
Object {
"_00_lines": Array [
"@param a - description1",
"@arg b - description2",
"@argument c - description3",
"@return description4",
"@readonly2",
],
"_01_gaps": Array [],
"_02_summarySection": Object {
"kind": "Section",
},
"_03_remarksBlock": undefined,
"_04_privateRemarksBlock": undefined,
"_05_deprecatedBlock": undefined,
"_06_paramBlocks": Array [
Object {
"kind": "ParamBlock",
"nodes": Array [
Object {
"kind": "BlockTag",
"nodes": Array [
Object {
"kind": "Excerpt: BlockTag",
"nodeExcerpt": "@param",
},
],
},
Object {
"kind": "Excerpt: Spacing",
"nodeExcerpt": " ",
},
Object {
"kind": "Excerpt: ParamBlock_ParameterName",
"nodeExcerpt": "a",
},
Object {
"kind": "Excerpt: Spacing",
"nodeExcerpt": " ",
},
Object {
"kind": "Excerpt: ParamBlock_Hyphen",
"nodeExcerpt": "-",
},
Object {
"kind": "Excerpt: Spacing",
"nodeExcerpt": " ",
},
Object {
"kind": "Section",
"nodes": Array [
Object {
"kind": "Paragraph",
"nodes": Array [
Object {
"kind": "PlainText",
"nodes": Array [
Object {
"kind": "Excerpt: PlainText",
"nodeExcerpt": "description1",
},
],
},
Object {
"kind": "SoftBreak",
"nodes": Array [
Object {
"kind": "Excerpt: SoftBreak",
"nodeExcerpt": "[n]",
},
],
},
],
},
],
},
],
},
Object {
"kind": "ParamBlock",
"nodes": Array [
Object {
"kind": "BlockTag",
"nodes": Array [
Object {
"kind": "Excerpt: BlockTag",
"nodeExcerpt": "@arg",
},
],
},
Object {
"kind": "Excerpt: Spacing",
"nodeExcerpt": " ",
},
Object {
"kind": "Excerpt: ParamBlock_ParameterName",
"nodeExcerpt": "b",
},
Object {
"kind": "Excerpt: Spacing",
"nodeExcerpt": " ",
},
Object {
"kind": "Excerpt: ParamBlock_Hyphen",
"nodeExcerpt": "-",
},
Object {
"kind": "Excerpt: Spacing",
"nodeExcerpt": " ",
},
Object {
"kind": "Section",
"nodes": Array [
Object {
"kind": "Paragraph",
"nodes": Array [
Object {
"kind": "PlainText",
"nodes": Array [
Object {
"kind": "Excerpt: PlainText",
"nodeExcerpt": "description2",
},
],
},
Object {
"kind": "SoftBreak",
"nodes": Array [
Object {
"kind": "Excerpt: SoftBreak",
"nodeExcerpt": "[n]",
},
],
},
],
},
],
},
],
},
Object {
"kind": "ParamBlock",
"nodes": Array [
Object {
"kind": "BlockTag",
"nodes": Array [
Object {
"kind": "Excerpt: BlockTag",
"nodeExcerpt": "@argument",
},
],
},
Object {
"kind": "Excerpt: Spacing",
"nodeExcerpt": " ",
},
Object {
"kind": "Excerpt: ParamBlock_ParameterName",
"nodeExcerpt": "c",
},
Object {
"kind": "Excerpt: Spacing",
"nodeExcerpt": " ",
},
Object {
"kind": "Excerpt: ParamBlock_Hyphen",
"nodeExcerpt": "-",
},
Object {
"kind": "Excerpt: Spacing",
"nodeExcerpt": " ",
},
Object {
"kind": "Section",
"nodes": Array [
Object {
"kind": "Paragraph",
"nodes": Array [
Object {
"kind": "PlainText",
"nodes": Array [
Object {
"kind": "Excerpt: PlainText",
"nodeExcerpt": "description3",
},
],
},
Object {
"kind": "SoftBreak",
"nodes": Array [
Object {
"kind": "Excerpt: SoftBreak",
"nodeExcerpt": "[n]",
},
],
},
],
},
],
},
],
},
],
"_07_typeParamBlocks": Array [],
"_08_returnsBlock": Object {
"kind": "Block",
"nodes": Array [
Object {
"kind": "BlockTag",
"nodes": Array [
Object {
"kind": "Excerpt: BlockTag",
"nodeExcerpt": "@return",
},
],
},
Object {
"kind": "Section",
"nodes": Array [
Object {
"kind": "Paragraph",
"nodes": Array [
Object {
"kind": "PlainText",
"nodes": Array [
Object {
"kind": "Excerpt: PlainText",
"nodeExcerpt": " description4",
},
],
},
Object {
"kind": "SoftBreak",
"nodes": Array [
Object {
"kind": "Excerpt: SoftBreak",
"nodeExcerpt": "[n]",
},
],
},
Object {
"kind": "SoftBreak",
"nodes": Array [
Object {
"kind": "Excerpt: SoftBreak",
"nodeExcerpt": "[n]",
},
],
},
],
},
],
},
],
},
"_09_customBlocks": Array [],
"_10_inheritDocTag": undefined,
"_11_modifierTags": Array [
Object {
"kind": "BlockTag",
"nodes": Array [
Object {
"kind": "Excerpt: BlockTag",
"nodeExcerpt": "@readonly2",
},
],
},
],
"_12_logMessages": Array [],
}
`;
273 changes: 236 additions & 37 deletions tsdoc/src/configuration/TSDocConfiguration.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
import { StandardTags } from '../details/StandardTags';
import { TSDocTagDefinition } from './TSDocTagDefinition';
import { TSDocTagDefinition, ITSDocTagDefinitionInternalParameters } from './TSDocTagDefinition';
import { TSDocValidationConfiguration } from './TSDocValidationConfiguration';
import { DocNodeManager } from './DocNodeManager';
import { BuiltInDocNodes } from '../nodes/BuiltInDocNodes';
import { TSDocMessageId, allTsdocMessageIds, allTsdocMessageIdsSet } from '../parser/TSDocMessageId';
import { TSDocSynonymCollection } from './TSDocSynonymCollection';
import { StringChecks } from '../parser/StringChecks';

interface ITSDocTagDefinitionOverride {
derivedTagDefinition: TSDocTagDefinition;
synonymCollection: TSDocSynonymCollection;
}

/**
* Configuration for the TSDocParser.
*/
export class TSDocConfiguration {
private readonly _tagDefinitions: TSDocTagDefinition[];
private readonly _tagDefinitionsByName: Map<string, TSDocTagDefinition>;
private readonly _supportedTagDefinitions: Set<TSDocTagDefinition>;
private readonly _baseTagDefinitionsByName: Map<string, TSDocTagDefinition>;
private readonly _baseTagDefinitions: TSDocTagDefinition[];
private readonly _supportedBaseTagDefinitions: Set<TSDocTagDefinition>;
private readonly _validation: TSDocValidationConfiguration;
private readonly _docNodeManager: DocNodeManager;
private _tagDefinitionOverrides: Map<TSDocTagDefinition, ITSDocTagDefinitionOverride> | undefined;
private _tagDefinitionOverridesReverseMap: Map<TSDocTagDefinition, TSDocTagDefinition> | undefined;
private _derivedTagDefinitions: TSDocTagDefinition[] | undefined;
private _supportedDerivedTagDefinitions: TSDocTagDefinition[] | undefined;

public constructor() {
this._tagDefinitions = [];
this._tagDefinitionsByName = new Map<string, TSDocTagDefinition>();
this._supportedTagDefinitions = new Set<TSDocTagDefinition>();
this._baseTagDefinitions = [];
this._baseTagDefinitionsByName = new Map<string, TSDocTagDefinition>();
this._supportedBaseTagDefinitions = new Set<TSDocTagDefinition>();
this._validation = new TSDocValidationConfiguration();
this._docNodeManager = new DocNodeManager();

@@ -36,7 +47,10 @@ export class TSDocConfiguration {
* The subset of "supported" tags is tracked by {@link TSDocConfiguration.supportedTagDefinitions}.
*/
public get tagDefinitions(): ReadonlyArray<TSDocTagDefinition> {
return this._tagDefinitions;
if (!this._derivedTagDefinitions) {
this._derivedTagDefinitions = this._baseTagDefinitions.map(tagDefinition => this.getConfiguredTagDefinition(tagDefinition));
}
return this._derivedTagDefinitions;
}

/**
@@ -48,7 +62,10 @@ export class TSDocConfiguration {
* {@link TSDocValidationConfiguration.reportUnsupportedTags} is enabled.
*/
public get supportedTagDefinitions(): ReadonlyArray<TSDocTagDefinition> {
return this.tagDefinitions.filter(x => this.isTagSupported(x));
if (!this._supportedDerivedTagDefinitions) {
this._supportedDerivedTagDefinitions = this.tagDefinitions.filter(x => this.isTagSupported(x));
}
return this._supportedDerivedTagDefinitions;
}

/**
@@ -70,15 +87,27 @@ export class TSDocConfiguration {
* if not found.
*/
public tryGetTagDefinition(tagName: string): TSDocTagDefinition | undefined {
return this._tagDefinitionsByName.get(tagName.toUpperCase());
return this.tryGetTagDefinitionWithUpperCase(tagName.toUpperCase());
}

/**
* Return the tag that was defined with the specified name, or undefined
* if not found.
*/
public tryGetTagDefinitionWithUpperCase(alreadyUpperCaseTagName: string): TSDocTagDefinition | undefined {
return this._tagDefinitionsByName.get(alreadyUpperCaseTagName);
const tagDefinition: TSDocTagDefinition | undefined
= this._baseTagDefinitionsByName.get(alreadyUpperCaseTagName);
return tagDefinition && this.getConfiguredTagDefinition(tagDefinition);
}

/**
* Return the configured version of a tag definition. If a tag definition has been configured
* with additional synonyms, the derived tag definition is returned.
*/
public getConfiguredTagDefinition(tagDefinition: TSDocTagDefinition): TSDocTagDefinition {
const override: ITSDocTagDefinitionOverride | undefined =
this._tagDefinitionOverrides && this._tagDefinitionOverrides.get(tagDefinition);
return override ? override.derivedTagDefinition : tagDefinition;
}

/**
@@ -89,20 +118,11 @@ export class TSDocConfiguration {
* If a tag is "defined" this means that the parser recognizes it and understands its syntax.
* Whereas if a tag is "supported", this means it is defined AND the application implements the tag.
*/
public addTagDefinition(tagDefinition: TSDocTagDefinition): void {
const existingDefinition: TSDocTagDefinition | undefined
= this._tagDefinitionsByName.get(tagDefinition.tagNameWithUpperCase);

if (existingDefinition === tagDefinition) {
return;
}

if (existingDefinition) {
throw new Error(`A tag is already defined using the name ${existingDefinition.tagName}`);
public addTagDefinition(tagDefinition: TSDocTagDefinition, supported?: boolean | undefined): void {
this._addTagDefinition(tagDefinition);
if (supported !== undefined) {
this.setSupportForTag(tagDefinition, supported);
}

this._tagDefinitions.push(tagDefinition);
this._tagDefinitionsByName.set(tagDefinition.tagNameWithUpperCase, tagDefinition);
}

/**
@@ -116,20 +136,17 @@ export class TSDocConfiguration {
supported?: boolean | undefined): void {

for (const tagDefinition of tagDefinitions) {
this.addTagDefinition(tagDefinition);

if (supported !== undefined) {
this.setSupportForTag(tagDefinition, supported);
}
this.addTagDefinition(tagDefinition, supported);
}
}

/**
* Returns true if the tag is supported in this configuration.
*/
public isTagSupported(tagDefinition: TSDocTagDefinition): boolean {
this._requireTagToBeDefined(tagDefinition);
return this._supportedTagDefinitions.has(tagDefinition);
const baseTagDefinition: TSDocTagDefinition = this._getBaseTagDefinition(tagDefinition);
this._requireTagToBeDefined(baseTagDefinition);
return this._supportedBaseTagDefinitions.has(baseTagDefinition);
}

/**
@@ -144,14 +161,16 @@ export class TSDocConfiguration {
* to true.
*/
public setSupportForTag(tagDefinition: TSDocTagDefinition, supported: boolean): void {
this._requireTagToBeDefined(tagDefinition);
const baseTagDefinition: TSDocTagDefinition = this._getBaseTagDefinition(tagDefinition);
this._requireTagToBeDefined(baseTagDefinition);
if (supported) {
this._supportedTagDefinitions.add(tagDefinition);
this._supportedBaseTagDefinitions.add(baseTagDefinition);
} else {
this._supportedTagDefinitions.delete(tagDefinition);
this._supportedBaseTagDefinitions.delete(baseTagDefinition);
}

this.validation.reportUnsupportedTags = true;
this._invalidateDerived();
}

/**
@@ -163,6 +182,101 @@ export class TSDocConfiguration {
}
}

/**
* Adds a synonym to a registered tag definition.
* @param tagDefinition - The tag definition to which to add a new synonym.
* @param synonyms - The synonyms to add.
* @returns The configured version of the provided tag definition.
*/
public addSynonym(tagDefinition: TSDocTagDefinition, ...synonyms: string[]): TSDocTagDefinition {
const baseTagDefinition: TSDocTagDefinition = this._getBaseTagDefinition(tagDefinition);
this._requireTagToBeDefined(baseTagDefinition);

const synonymsWithUpperCase: string[] = synonyms.map(synonym => synonym.toUpperCase());
const synonymsToAdd: string[] = [];
const synonymsWithUpperCaseToAdd: string[] = [];
for (let i: number = 0; i < synonyms.length; i++) {
const synonym: string = synonyms[i];
const synonymWithUpperCase: string = synonymsWithUpperCase[i];
StringChecks.validateTSDocTagName(synonym);

const existingDefinition: TSDocTagDefinition | undefined
= this._baseTagDefinitionsByName.get(synonymWithUpperCase);

if (existingDefinition) {
if (existingDefinition !== baseTagDefinition) {
throw new Error(`A tag is already defined using the name ${synonym}`);
}
continue;
}

synonymsToAdd.push(synonym);
synonymsWithUpperCaseToAdd.push(synonymWithUpperCase);
}

if (synonymsToAdd.length === 0) {
return this.getConfiguredTagDefinition(baseTagDefinition);
}

const override: ITSDocTagDefinitionOverride = this._overrideTagDefinition(baseTagDefinition);
for (let i: number = 0; i < synonymsToAdd.length; i++) {
override.synonymCollection.add(synonymsToAdd[i]);
this._baseTagDefinitionsByName.set(synonymsWithUpperCaseToAdd[i], baseTagDefinition);
}

return override.derivedTagDefinition;
}

/**
* Removes a synonym from a registered tag definition.
* @param tagDefinition - The tag definition from which to remove a synonym.
* @param synonyms - The synonyms to remove.
* @returns The configured version of the provided tag definition.
*/
public removeSynonym(tagDefinition: TSDocTagDefinition, ...synonyms: string[]): TSDocTagDefinition {
const baseTagDefinition: TSDocTagDefinition = this._getBaseTagDefinition(tagDefinition);
this._requireTagToBeDefined(baseTagDefinition);

const synonymsWithUpperCase: string[] = synonyms.map(synonym => synonym.toUpperCase());
const synonymsToRemove: string[] = [];
const synonymsWithUpperCaseToRemove: string[] = [];
for (let i: number = 0; i < synonyms.length; i++) {
const synonym: string = synonyms[i];
const synonymWithUpperCase: string = synonymsWithUpperCase[i];
StringChecks.validateTSDocTagName(synonym);

const existingDefinition: TSDocTagDefinition | undefined
= this._baseTagDefinitionsByName.get(synonymWithUpperCase);

if (!existingDefinition) {
continue;
}

if (existingDefinition !== baseTagDefinition) {
throw new Error(`The synonym ${synonym} is not provided by this tag.`);
}

if (baseTagDefinition.tagNameWithUpperCase === synonymWithUpperCase) {
throw new Error(`${synonym} is the primary tag name for this definition and cannot be removed.`);
}

synonymsToRemove.push(synonym);
synonymsWithUpperCaseToRemove.push(synonymWithUpperCase);
}

if (synonymsToRemove.length === 0) {
return this.getConfiguredTagDefinition(baseTagDefinition);
}

const override: ITSDocTagDefinitionOverride = this._overrideTagDefinition(baseTagDefinition);
for (let i: number = 0; i < synonymsToRemove.length; i++) {
override.synonymCollection.delete(synonymsToRemove[i]);
this._baseTagDefinitionsByName.delete(synonymsWithUpperCaseToRemove[i]);
}

return override.derivedTagDefinition;
}

/**
* Returns true if the specified {@link TSDocMessageId} string is implemented by this release of the TSDoc parser.
* This can be used to detect misspelled identifiers.
@@ -188,14 +302,99 @@ export class TSDocConfiguration {
return allTsdocMessageIds as ReadonlyArray<TSDocMessageId>;
}

private _requireTagToBeDefined(tagDefinition: TSDocTagDefinition): void {
private _requireTagToBeDefined(baseTagDefinition: TSDocTagDefinition): void {
const matching: TSDocTagDefinition | undefined
= this._tagDefinitionsByName.get(tagDefinition.tagNameWithUpperCase);
= this._baseTagDefinitionsByName.get(baseTagDefinition.tagNameWithUpperCase);
if (matching) {
if (matching === tagDefinition) {
if (matching === baseTagDefinition) {
return;
}
}
throw new Error('The specified TSDocTagDefinition is not defined for this TSDocConfiguration');
}

private _getBaseTagDefinition(tagDefinition: TSDocTagDefinition): TSDocTagDefinition {
return this._tagDefinitionOverridesReverseMap &&
this._tagDefinitionOverridesReverseMap.get(tagDefinition) || tagDefinition;
}

private _addTagDefinition(tagDefinition: TSDocTagDefinition): void {
const baseTagDefinition: TSDocTagDefinition = this._getBaseTagDefinition(tagDefinition);
const existingDefinition: TSDocTagDefinition | undefined
= this._baseTagDefinitionsByName.get(baseTagDefinition.tagNameWithUpperCase);

if (existingDefinition === baseTagDefinition) {
return;
}

if (existingDefinition) {
throw new Error(`A tag is already defined using the name ${existingDefinition.tagName}`);
}

const synonyms: ReadonlyArray<string> = baseTagDefinition.synonyms;
const synonymsWithUpperCase: ReadonlyArray<string> = baseTagDefinition.synonymsWithUpperCase;
const synonymsToAdd: string[] = [];
for (let i: number = 0; i < synonymsWithUpperCase.length; i++) {
const synonymWithUpperCase: string = synonymsWithUpperCase[i];
const existingDefinition: TSDocTagDefinition | undefined
= this._baseTagDefinitionsByName.get(synonymWithUpperCase);
if (existingDefinition) {
if (existingDefinition !== baseTagDefinition) {
throw new Error(`A tag is already defined using the name ${synonyms[i]}`);
}
continue;
}
synonymsToAdd.push(synonymWithUpperCase);
}

this._baseTagDefinitions.push(baseTagDefinition);
this._baseTagDefinitionsByName.set(baseTagDefinition.tagNameWithUpperCase, baseTagDefinition);
for (const synonym of synonymsToAdd) {
this._baseTagDefinitionsByName.set(synonym, baseTagDefinition);
}

this._invalidateDerived();
}

private _overrideTagDefinition(baseTagDefinition: TSDocTagDefinition): ITSDocTagDefinitionOverride {
if (!this._tagDefinitionOverrides) {
this._tagDefinitionOverrides = new Map<TSDocTagDefinition, ITSDocTagDefinitionOverride>();
}
if (!this._tagDefinitionOverridesReverseMap) {
this._tagDefinitionOverridesReverseMap = new Map<TSDocTagDefinition, TSDocTagDefinition>();
}

let override: ITSDocTagDefinitionOverride | undefined =
this._tagDefinitionOverrides.get(baseTagDefinition);

if (!override) {
const synonymCollection: TSDocSynonymCollection = new TSDocSynonymCollection();
const derivedTagParameters: ITSDocTagDefinitionInternalParameters = {
tagName: baseTagDefinition.tagName,
syntaxKind: baseTagDefinition.syntaxKind,
allowMultiple: baseTagDefinition.allowMultiple,
standardization: baseTagDefinition.standardization,
synonyms: baseTagDefinition.synonyms.slice(),
synonymCollection,
};

const derivedTagDefinition: TSDocTagDefinition = new TSDocTagDefinition(derivedTagParameters);
override = { derivedTagDefinition, synonymCollection };

this._tagDefinitionOverrides.set(baseTagDefinition, override);
this._tagDefinitionOverridesReverseMap.set(derivedTagDefinition, baseTagDefinition);
this._invalidateDerived();
}

return override;
}

private _invalidateDerived(): void {
if (this._derivedTagDefinitions) {
this._derivedTagDefinitions = undefined;
}
if (this._supportedDerivedTagDefinitions) {
this._supportedDerivedTagDefinitions = undefined;
}
}
}
67 changes: 67 additions & 0 deletions tsdoc/src/configuration/TSDocSynonymCollection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { StringChecks } from "../parser/StringChecks";

/**
* @internal
*/
export class TSDocSynonymCollection {
private _synonyms: string[];
private _synonymsWithUpperCase: string[] | undefined;

public constructor() {
this._synonyms = [];
this._synonymsWithUpperCase = undefined;
}

public get count(): number {
return this._synonyms.length;
}

public get synonyms(): ReadonlyArray<string> {
return this._synonyms;
}

public get synonymsWithUpperCase(): ReadonlyArray<string> {
if (!this._synonymsWithUpperCase) {
this._synonymsWithUpperCase = this._synonyms.map(synonym => synonym.toUpperCase());
}
return this._synonymsWithUpperCase;
}

public add(synonym: string): void {
StringChecks.validateTSDocTagName(synonym);
if (this._synonyms.indexOf(synonym) >= 0) {
return;
}
this._synonyms.push(synonym);
this._invalidateSynonymsWithUpperCase();
}

public delete(synonym: string): boolean {
const index: number = this._synonyms.indexOf(synonym);
if (index >= 0) {
this._synonyms.splice(index, 1);
this._invalidateSynonymsWithUpperCase();
return true;
}
return false;
}

public clear(): void {
this._synonyms.length = 0;
this._invalidateSynonymsWithUpperCase();
}

public hasTagName(tagName: string): boolean {
return this.synonymsWithUpperCase.indexOf(tagName.toUpperCase()) >= 0;
}

public [Symbol.iterator](): IterableIterator<string> {
return this._synonyms[Symbol.iterator]();
}

private _invalidateSynonymsWithUpperCase(): void {
if (this._synonymsWithUpperCase) {
this._synonymsWithUpperCase = undefined;
}
}
}
50 changes: 50 additions & 0 deletions tsdoc/src/configuration/TSDocTagDefinition.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { StringChecks } from '../parser/StringChecks';
import { Standardization } from '../details/Standardization';
import { DocBlockTag, DocInlineTagBase } from '../nodes';
import { TSDocSynonymCollection } from './TSDocSynonymCollection';

/**
* Determines the type of syntax for a TSDocTagDefinition
@@ -29,6 +31,7 @@ export enum TSDocTagSyntaxKind {
export interface ITSDocTagDefinitionParameters {
tagName: string;
syntaxKind: TSDocTagSyntaxKind;
synonyms?: string[];
allowMultiple?: boolean;
}

@@ -37,6 +40,7 @@ export interface ITSDocTagDefinitionParameters {
*/
export interface ITSDocTagDefinitionInternalParameters extends ITSDocTagDefinitionParameters {
standardization: Standardization;
synonymCollection?: TSDocSynonymCollection;
}

/**
@@ -72,6 +76,8 @@ export class TSDocTagDefinition {
*/
public readonly allowMultiple: boolean;

private _synonymCollection: TSDocSynonymCollection;

public constructor(parameters: ITSDocTagDefinitionParameters) {
StringChecks.validateTSDocTagName(parameters.tagName);
this.tagName = parameters.tagName;
@@ -80,5 +86,49 @@ export class TSDocTagDefinition {
this.standardization = (parameters as ITSDocTagDefinitionInternalParameters).standardization
|| Standardization.None;
this.allowMultiple = !!parameters.allowMultiple;
this._synonymCollection = (parameters as ITSDocTagDefinitionInternalParameters).synonymCollection ||
new TSDocSynonymCollection();
if (parameters.synonyms) {
for (const synonym of parameters.synonyms) {
if (synonym !== this.tagName) {
this._synonymCollection.add(synonym);
}
}
}
}

/**
* Synonyms for the TSDoc tag. TSDoc tag names start with an at-sign ("@") followed
* by ASCII letters using "camelCase" capitalization.
*/
public get synonyms(): ReadonlyArray<string> {
return this._synonymCollection.synonyms;
}

/**
* Synonyms for the TSDoc tag in all capitals, which is used for performing
* case-insensitive comparisons or lookups.
*/
public get synonymsWithUpperCase(): ReadonlyArray<string> {
return this._synonymCollection.synonymsWithUpperCase;
}

/**
* Returns whether this tag definition is a definition of the provided tag.
*/
public isDefinitionOfTag(tag: DocBlockTag | DocInlineTagBase): boolean {
const hasCorrectKind: boolean = this.syntaxKind === TSDocTagSyntaxKind.InlineTag ?
tag instanceof DocInlineTagBase :
tag instanceof DocBlockTag;
return hasCorrectKind && this.hasTagName(tag.tagNameWithUpperCase);
}

/**
* Returns whether the provided tag name is defined by this tag definition.
*/
public hasTagName(tagName: string): boolean {
const tagNameWithUpperCase: string = tagName.toUpperCase();
return this.tagNameWithUpperCase === tagNameWithUpperCase ||
this.synonymsWithUpperCase.indexOf(tagNameWithUpperCase) >= 0;
}
}
247 changes: 247 additions & 0 deletions tsdoc/src/configuration/__tests__/TSDocConfiguration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import { TSDocConfiguration } from '../TSDocConfiguration';
import { TSDocTagDefinition, TSDocTagSyntaxKind } from '../TSDocTagDefinition';

describe('Synonym overrides', () => {
describe('addSynonym', () => {
describe('with no existing synonym in base', () => {
let configuration: TSDocConfiguration;
let baseTag: TSDocTagDefinition;
let derivedTag: TSDocTagDefinition;
beforeEach(() => {
configuration = new TSDocConfiguration();
baseTag = new TSDocTagDefinition({
syntaxKind: TSDocTagSyntaxKind.BlockTag,
tagName: '@foo',
});
configuration.addTagDefinition(baseTag);
configuration.setSupportForTag(baseTag, /*supported*/ true);
derivedTag = configuration.addSynonym(baseTag, '@bar');
afterEach(() => {
configuration = undefined!;
baseTag = undefined!;
derivedTag = undefined!;
});
});
test('does not mutate base tag', () => {
expect(baseTag.synonyms).toHaveLength(0);
});
test('returns a derived tag', () => {
expect(derivedTag).not.toBe(baseTag);
});
test('derived tag has expected synonyms', () => {
expect(derivedTag.synonyms).toEqual(['@bar']);
});
test('derived tag differs from base only in synonyms', () => {
expect(derivedTag.tagName).toEqual(baseTag.tagName);
expect(derivedTag.tagNameWithUpperCase).toEqual(baseTag.tagNameWithUpperCase);
expect(derivedTag.syntaxKind).toEqual(baseTag.syntaxKind);
expect(derivedTag.standardization).toEqual(baseTag.standardization);
expect(derivedTag.allowMultiple).toEqual(baseTag.allowMultiple);
expect(derivedTag.synonyms).not.toEqual(baseTag.synonyms);
});
test('additional synonym for base returns derived', () => {
expect(configuration.addSynonym(baseTag, '@baz')).toBe(derivedTag);
});
test('additional synonym for derived returns derived', () => {
expect(configuration.addSynonym(derivedTag, '@baz')).toBe(derivedTag);
});
test('additional synonym for derived mutates derived', () => {
configuration.addSynonym(derivedTag, '@baz')
expect(derivedTag.synonyms).toEqual(['@bar', '@baz']);
});
test('derived replaces base in tagDefinitions', () => {
expect(configuration.tagDefinitions).toHaveLength(1);
expect(configuration.tagDefinitions[0]).toBe(derivedTag);
});
test('derived replaces base in supportedTagDefinitions', () => {
expect(configuration.supportedTagDefinitions).toHaveLength(1);
expect(configuration.supportedTagDefinitions[0]).toBe(derivedTag);
});
test('derived tag reachable by name', () => {
expect(configuration.tryGetTagDefinition('@foo')).toBe(derivedTag);
});
test('derived tag reachable by synonym', () => {
expect(configuration.tryGetTagDefinition('@bar')).toBe(derivedTag);
});
test('configured tag of base is derived tag', () => {
expect(configuration.getConfiguredTagDefinition(baseTag)).toBe(derivedTag);
});
test('configured tag of derived is derived tag', () => {
expect(configuration.getConfiguredTagDefinition(derivedTag)).toBe(derivedTag);
});
});
describe('for existing synonym in base', () => {
let configuration: TSDocConfiguration;
let baseTag: TSDocTagDefinition;
let baseTagAfterAddExisting: TSDocTagDefinition;
beforeEach(() => {
configuration = new TSDocConfiguration();
baseTag = new TSDocTagDefinition({
syntaxKind: TSDocTagSyntaxKind.BlockTag,
tagName: '@foo',
synonyms: ['@bar']
});
configuration.addTagDefinition(baseTag);
configuration.setSupportForTag(baseTag, /*supported*/ true);
baseTagAfterAddExisting = configuration.addSynonym(baseTag, '@bar');
afterEach(() => {
configuration = undefined!;
baseTag = undefined!;
baseTagAfterAddExisting = undefined!;
});
});
test('does not modify base tag', () => {
expect(baseTag.synonyms).toEqual(['@bar']);
});
test('returns the base tag', () => {
expect(baseTagAfterAddExisting).toBe(baseTag);
});
test('base tag reachable by name', () => {
expect(configuration.tryGetTagDefinition('@foo')).toBe(baseTag);
});
test('base tag reachable by synonym', () => {
expect(configuration.tryGetTagDefinition('@bar')).toBe(baseTag);
});
test('configured tag of base is base tag', () => {
expect(configuration.getConfiguredTagDefinition(baseTag)).toBe(baseTag);
});
describe('additional synonym', () => {
let derivedTag: TSDocTagDefinition;
beforeEach(() => {
derivedTag = configuration.addSynonym(baseTag, '@baz');
afterEach(() => {
derivedTag = undefined!;
});
});
test('does not modify base tag', () => {
expect(baseTag.synonyms).toEqual(['@bar']);
});
test('returns a derived tag', () => {
expect(derivedTag).not.toBe(baseTag);
});
test('does not modify base tag', () => {
expect(baseTag.synonyms).toHaveLength(0);
});
test('derived replaces base in tagDefinitions', () => {
expect(configuration.tagDefinitions).toHaveLength(1);
expect(configuration.tagDefinitions[0]).toBe(derivedTag);
});
test('derived replaces base in supportedTagDefinitions', () => {
expect(configuration.supportedTagDefinitions).toHaveLength(1);
expect(configuration.supportedTagDefinitions[0]).toBe(derivedTag);
});
test('derived tag reachable by name', () => {
expect(configuration.tryGetTagDefinition('@foo')).toBe(derivedTag);
});
test('derived tag reachable by synonym', () => {
expect(configuration.tryGetTagDefinition('@baz')).toBe(derivedTag);
});
test('configured tag of base is derived tag', () => {
expect(configuration.getConfiguredTagDefinition(baseTag)).toBe(derivedTag);
});
test('configured tag of derived is derived tag', () => {
expect(configuration.getConfiguredTagDefinition(derivedTag)).toBe(derivedTag);
});
});
});
});
describe('removeSynonym', () => {
describe('with no existing synonym in base', () => {
let configuration: TSDocConfiguration;
let baseTag: TSDocTagDefinition;
let derivedTag: TSDocTagDefinition;
let derivedTagAfterRemove: TSDocTagDefinition;
beforeEach(() => {
configuration = new TSDocConfiguration();
baseTag = new TSDocTagDefinition({
syntaxKind: TSDocTagSyntaxKind.BlockTag,
tagName: '@foo',
});
configuration.addTagDefinition(baseTag);
derivedTag = configuration.addSynonym(baseTag, '@bar');
derivedTagAfterRemove = configuration.removeSynonym(baseTag, '@bar');
afterEach(() => {
configuration = undefined!;
baseTag = undefined!;
derivedTag = undefined!;
derivedTagAfterRemove = undefined!;
});
});
test('returned tag remains derived', () => {
expect(derivedTagAfterRemove).toBe(derivedTag);
});
test('mutates synonyms on derived', () => {
expect(derivedTag.synonyms).toHaveLength(0);
});
test('derived replaces base in tagDefinitions', () => {
expect(configuration.tagDefinitions).toHaveLength(1);
expect(configuration.tagDefinitions[0]).toBe(derivedTag);
});
test('derived replaces base in supportedTagDefinitions', () => {
expect(configuration.supportedTagDefinitions).toHaveLength(1);
expect(configuration.supportedTagDefinitions[0]).toBe(derivedTag);
});
test('derived tag reachable by name', () => {
expect(configuration.tryGetTagDefinition('@foo')).toBe(derivedTag);
});
test('nothing reachable by synonym', () => {
expect(configuration.tryGetTagDefinition('@bar')).toBeUndefined();
});
test('configured tag of base is derived tag', () => {
expect(configuration.getConfiguredTagDefinition(baseTag)).toBe(derivedTag);
});
test('configured tag of derived is derived tag', () => {
expect(configuration.getConfiguredTagDefinition(derivedTag)).toBe(derivedTag);
});
});
describe('with existing synonym in base', () => {
let configuration: TSDocConfiguration;
let baseTag: TSDocTagDefinition;
let derivedTag: TSDocTagDefinition;
beforeEach(() => {
configuration = new TSDocConfiguration();
baseTag = new TSDocTagDefinition({
syntaxKind: TSDocTagSyntaxKind.BlockTag,
tagName: '@foo',
synonyms: ['@bar']
});
configuration.addTagDefinition(baseTag);
derivedTag = configuration.removeSynonym(baseTag, '@bar');
afterEach(() => {
configuration = undefined!;
baseTag = undefined!;
derivedTag = undefined!;
});
});
test('does not mutate base tag', () => {
expect(baseTag.synonyms).toEqual(['@bar']);
});
test('returns a derived tag', () => {
expect(derivedTag).not.toBe(baseTag);
});
test('derived tag has expected synonyms', () => {
expect(derivedTag.synonyms).toHaveLength(0);
});
test('derived replaces base in tagDefinitions', () => {
expect(configuration.tagDefinitions).toHaveLength(1);
expect(configuration.tagDefinitions[0]).toBe(derivedTag);
});
test('derived replaces base in supportedTagDefinitions', () => {
expect(configuration.supportedTagDefinitions).toHaveLength(1);
expect(configuration.supportedTagDefinitions[0]).toBe(derivedTag);
});
test('derived tag reachable by name', () => {
expect(configuration.tryGetTagDefinition('@foo')).toBe(derivedTag);
});
test('nothing reachable by synonym', () => {
expect(configuration.tryGetTagDefinition('@bar')).toBeUndefined();
});
test('configured tag of base is derived tag', () => {
expect(configuration.getConfiguredTagDefinition(baseTag)).toBe(derivedTag);
});
test('configured tag of derived is derived tag', () => {
expect(configuration.getConfiguredTagDefinition(derivedTag)).toBe(derivedTag);
});
});
});
});
35 changes: 32 additions & 3 deletions tsdoc/src/details/ModifierTagSet.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { DocBlockTag } from '../nodes/DocBlockTag';
import { TSDocTagDefinition, TSDocTagSyntaxKind } from '../configuration/TSDocTagDefinition';
import { TSDocConfiguration } from '../configuration/TSDocConfiguration';

/**
* Constructor parameters for {@link ModifierTagSet}.
*/
export interface IModifierTagSetParameters {
configuration: TSDocConfiguration;
}

/**
* Represents a set of modifier tags that were extracted from a doc comment.
@@ -11,13 +19,23 @@ import { TSDocTagDefinition, TSDocTagSyntaxKind } from '../configuration/TSDocTa
* signature is internal (i.e. not part of the public API contract).
*/
export class ModifierTagSet {
public readonly configuration: TSDocConfiguration;

private readonly _nodes: DocBlockTag[] = [];

// NOTE: To implement case insensitivity, the keys in this set are always upper-case.
// This convention makes the normalization more obvious (and as a general practice handles
// the Turkish "i" character correctly).
private readonly _nodesByName: Map<string, DocBlockTag> = new Map<string, DocBlockTag>();

/**
* Don't call this directly. Instead use {@link TSDocParser}
* @internal
*/
public constructor(parameters: IModifierTagSetParameters) {
this.configuration = parameters.configuration;
}

/**
* The original block tag nodes that defined the modifiers in this set, excluding duplicates.
*/
@@ -36,8 +54,7 @@ export class ModifierTagSet {

/**
* Returns true if the set contains a DocBlockTag matching the specified tag definition.
* Note that synonyms are not considered. The comparison is case-insensitive.
* The TSDocTagDefinition must be a modifier tag.
* The comparison is case-insensitive. The TSDocTagDefinition must be a modifier tag.
* @param tagName - The name of the tag, including the `@` prefix For example, `@internal`
*/
public hasTag(modifierTagDefinition: TSDocTagDefinition): boolean {
@@ -53,7 +70,19 @@ export class ModifierTagSet {
if (modifierTagDefinition.syntaxKind !== TSDocTagSyntaxKind.ModifierTag) {
throw new Error('The tag definition is not a modifier tag');
}
return this._nodesByName.get(modifierTagDefinition.tagNameWithUpperCase);

const configuredTagDefinition: TSDocTagDefinition
= this.configuration.getConfiguredTagDefinition(modifierTagDefinition);

let tag: DocBlockTag | undefined =
this._nodesByName.get(configuredTagDefinition.tagNameWithUpperCase);
if (!tag) {
for (const synonym of configuredTagDefinition.synonymsWithUpperCase) {
tag = this._nodesByName.get(synonym);
if (tag) break;
}
}
return tag;
}

/**
5 changes: 5 additions & 0 deletions tsdoc/src/details/StandardTags.ts
Original file line number Diff line number Diff line change
@@ -68,6 +68,7 @@ export class StandardTags {
*/
public static readonly defaultValue: TSDocTagDefinition = StandardTags._defineTag({
tagName: '@defaultValue',
synonyms: ['@default'],
syntaxKind: TSDocTagSyntaxKind.BlockTag,
standardization: Standardization.Extended
});
@@ -234,6 +235,7 @@ export class StandardTags {
*/
public static readonly param: TSDocTagDefinition = StandardTags._defineTag({
tagName: '@param',
synonyms: ['@arg', '@argument'],
syntaxKind: TSDocTagSyntaxKind.BlockTag,
allowMultiple: true,
standardization: Standardization.Core
@@ -307,6 +309,7 @@ export class StandardTags {
*/
public static readonly returns: TSDocTagDefinition = StandardTags._defineTag({
tagName: '@returns',
synonyms: ['@return'],
syntaxKind: TSDocTagSyntaxKind.BlockTag,
standardization: Standardization.Core
});
@@ -361,6 +364,7 @@ export class StandardTags {
*/
public static readonly throws: TSDocTagDefinition = StandardTags._defineTag({
tagName: '@throws',
synonyms: ['@exception'],
syntaxKind: TSDocTagSyntaxKind.BlockTag,
allowMultiple: true,
standardization: Standardization.Extended
@@ -375,6 +379,7 @@ export class StandardTags {
*/
public static readonly typeParam: TSDocTagDefinition = StandardTags._defineTag({
tagName: '@typeParam',
synonyms: ['@template'],
syntaxKind: TSDocTagSyntaxKind.BlockTag,
allowMultiple: true,
standardization: Standardization.Core
2 changes: 1 addition & 1 deletion tsdoc/src/emitters/TSDocEmitter.ts
Original file line number Diff line number Diff line change
@@ -96,7 +96,7 @@ export class TSDocEmitter {
this._ensureLineSkipped();
this._renderNode(docBlock.blockTag);

if (docBlock.blockTag.tagNameWithUpperCase === StandardTags.returns.tagNameWithUpperCase) {
if (StandardTags.returns.isDefinitionOfTag(docBlock.blockTag)) {
this._writeContent(' ');
this._hangingParagraph = true;
}
4 changes: 2 additions & 2 deletions tsdoc/src/nodes/DocComment.ts
Original file line number Diff line number Diff line change
@@ -103,8 +103,7 @@ export class DocComment extends DocNode {
this.params = new DocParamCollection({ configuration: this.configuration });
this.typeParams = new DocParamCollection({ configuration: this.configuration });
this.returnsBlock = undefined;

this.modifierTagSet = new StandardModifierTagSet();
this.modifierTagSet = new StandardModifierTagSet({ configuration: this.configuration });

this._customBlocks = [];
}
@@ -167,3 +166,4 @@ export class DocComment extends DocNode {

// Circular reference
import { TSDocEmitter } from '../emitters/TSDocEmitter';

3 changes: 2 additions & 1 deletion tsdoc/src/nodes/DocLinkTag.ts
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import {
} from './DocInlineTagBase';
import { DocExcerpt, ExcerptKind } from './DocExcerpt';
import { TokenSequence } from '../parser/TokenSequence';
import { StandardTags } from '../details/StandardTags';

/**
* Constructor parameters for {@link DocLinkTag}.
@@ -62,7 +63,7 @@ export class DocLinkTag extends DocInlineTagBase {
public constructor(parameters: IDocLinkTagParameters | IDocLinkTagParsedParameters) {
super(parameters);

if (this.tagNameWithUpperCase !== '@LINK') {
if (!StandardTags.link.hasTagName(this.tagNameWithUpperCase)) {
throw new Error('DocLinkTag requires the tag name to be "{@link}"');
}

45 changes: 18 additions & 27 deletions tsdoc/src/parser/NodeParser.ts
Original file line number Diff line number Diff line change
@@ -276,14 +276,14 @@ export class NodeParser {
if (tagDefinition) {
switch (tagDefinition.syntaxKind) {
case TSDocTagSyntaxKind.BlockTag:
if (docBlockTag.tagNameWithUpperCase === StandardTags.param.tagNameWithUpperCase) {
if (StandardTags.param.isDefinitionOfTag(docBlockTag)) {
const docParamBlock: DocParamBlock = this._parseParamBlock(tokenReader, docBlockTag, StandardTags.param.tagName);

this._parserContext.docComment.params.add(docParamBlock);

this._currentSection = docParamBlock.content;
return;
} else if (docBlockTag.tagNameWithUpperCase === StandardTags.typeParam.tagNameWithUpperCase) {
} else if (StandardTags.typeParam.isDefinitionOfTag(docBlockTag)) {
const docParamBlock: DocParamBlock = this._parseParamBlock(tokenReader, docBlockTag, StandardTags.typeParam.tagName);

this._parserContext.docComment.typeParams.add(docParamBlock);
@@ -315,22 +315,16 @@ export class NodeParser {

private _addBlockToDocComment(block: DocBlock): void {
const docComment: DocComment = this._parserContext.docComment;

switch (block.blockTag.tagNameWithUpperCase) {
case StandardTags.remarks.tagNameWithUpperCase:
docComment.remarksBlock = block;
break;
case StandardTags.privateRemarks.tagNameWithUpperCase:
docComment.privateRemarks = block;
break;
case StandardTags.deprecated.tagNameWithUpperCase:
docComment.deprecatedBlock = block;
break;
case StandardTags.returns.tagNameWithUpperCase:
docComment.returnsBlock = block;
break;
default:
docComment.appendCustomBlock(block);
if (StandardTags.remarks.isDefinitionOfTag(block.blockTag)) {
docComment.remarksBlock = block;
} else if (StandardTags.privateRemarks.isDefinitionOfTag(block.blockTag)) {
docComment.privateRemarks = block;
} else if (StandardTags.deprecated.isDefinitionOfTag(block.blockTag)) {
docComment.deprecatedBlock = block;
} else if (StandardTags.returns.isDefinitionOfTag(block.blockTag)) {
docComment.returnsBlock = block;
} else {
docComment.appendCustomBlock(block);
}
}

@@ -816,15 +810,12 @@ export class NodeParser {
tagContentExcerpt ? tagContentExcerpt : TokenSequence.createEmpty(this._parserContext));

let docNode: DocNode;
switch (tagNameWithUpperCase) {
case StandardTags.inheritDoc.tagNameWithUpperCase:
docNode = this._parseInheritDocTag(docInlineTagParsedParameters, embeddedTokenReader);
break;
case StandardTags.link.tagNameWithUpperCase:
docNode = this._parseLinkTag(docInlineTagParsedParameters, embeddedTokenReader);
break;
default:
docNode = new DocInlineTag(docInlineTagParsedParameters);
if (StandardTags.inheritDoc.hasTagName(tagNameWithUpperCase)) {
docNode = this._parseInheritDocTag(docInlineTagParsedParameters, embeddedTokenReader);
} else if (StandardTags.link.hasTagName(tagNameWithUpperCase)) {
docNode = this._parseLinkTag(docInlineTagParsedParameters, embeddedTokenReader);
} else {
docNode = new DocInlineTag(docInlineTagParsedParameters);
}

// Validate the tag