From aa35097379e26c6630e28164b71892f19581a3c3 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 20 Oct 2017 08:11:40 -0700 Subject: [PATCH] Added tags logic for completed-docs (#2415) --- src/rules/completed-docs/blockExclusion.ts | 42 +++ src/rules/completed-docs/classExclusion.ts | 68 +++++ src/rules/completed-docs/exclusion.ts | 35 +++ .../completed-docs/exclusionDescriptors.ts | 33 ++ src/rules/completed-docs/exclusionFactory.ts | 65 ++++ src/rules/completed-docs/tagExclusion.ts | 99 ++++++ src/rules/completedDocsRule.ts | 285 ++++++++---------- .../completed-docs/tags/content/test.ts.lint | 30 ++ .../completed-docs/tags/content/tsconfig.json | 5 + .../completed-docs/tags/content/tslint.json | 19 ++ .../tags/existence/test.ts.lint | 23 ++ .../tags/existence/tsconfig.json | 5 + .../completed-docs/tags/existence/tslint.json | 17 ++ 13 files changed, 571 insertions(+), 155 deletions(-) create mode 100644 src/rules/completed-docs/blockExclusion.ts create mode 100644 src/rules/completed-docs/classExclusion.ts create mode 100644 src/rules/completed-docs/exclusion.ts create mode 100644 src/rules/completed-docs/exclusionDescriptors.ts create mode 100644 src/rules/completed-docs/exclusionFactory.ts create mode 100644 src/rules/completed-docs/tagExclusion.ts create mode 100644 test/rules/completed-docs/tags/content/test.ts.lint create mode 100644 test/rules/completed-docs/tags/content/tsconfig.json create mode 100644 test/rules/completed-docs/tags/content/tslint.json create mode 100644 test/rules/completed-docs/tags/existence/test.ts.lint create mode 100644 test/rules/completed-docs/tags/existence/tsconfig.json create mode 100644 test/rules/completed-docs/tags/existence/tslint.json diff --git a/src/rules/completed-docs/blockExclusion.ts b/src/rules/completed-docs/blockExclusion.ts new file mode 100644 index 00000000000..b0578caf41f --- /dev/null +++ b/src/rules/completed-docs/blockExclusion.ts @@ -0,0 +1,42 @@ +/** + * @license + * Copyright 2017 Palantir Technologies, Inc. + * + * 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 + * + * http://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. + */ + +import * as ts from "typescript"; + +import * as Lint from "../../index"; +import { ALL, Visibility, VISIBILITY_EXPORTED, VISIBILITY_INTERNAL } from "../completedDocsRule"; +import { Exclusion } from "./exclusion"; + +export interface IBlockExclusionDescriptor { + visibilities?: Visibility[]; +} + +export class BlockExclusion extends Exclusion { + public readonly visibilities: Set = this.createSet(this.descriptor.visibilities); + + public excludes(node: ts.Node) { + if (this.visibilities.has(ALL)) { + return false; + } + + if (Lint.hasModifier(node.modifiers, ts.SyntaxKind.ExportKeyword)) { + return !this.visibilities.has(VISIBILITY_EXPORTED); + } + + return !this.visibilities.has(VISIBILITY_INTERNAL); + } +} diff --git a/src/rules/completed-docs/classExclusion.ts b/src/rules/completed-docs/classExclusion.ts new file mode 100644 index 00000000000..f751edb9144 --- /dev/null +++ b/src/rules/completed-docs/classExclusion.ts @@ -0,0 +1,68 @@ +/** + * @license + * Copyright 2013 Palantir Technologies, Inc. + * + * 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 + * + * http://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. + */ + +import * as ts from "typescript"; + +import * as Lint from "../../index"; +import { + ALL, Location, LOCATION_INSTANCE, LOCATION_STATIC, Privacy, PRIVACY_PRIVATE, PRIVACY_PROTECTED, PRIVACY_PUBLIC, +} from "../completedDocsRule"; +import { Exclusion } from "./exclusion"; + +export interface IClassExclusionDescriptor { + locations?: Location[]; + privacies?: Privacy[]; +} + +export class ClassExclusion extends Exclusion { + public readonly locations: Set = this.createSet(this.descriptor.locations); + public readonly privacies: Set = this.createSet(this.descriptor.privacies); + + public excludes(node: ts.Node) { + return !( + this.shouldLocationBeDocumented(node) + && this.shouldPrivacyBeDocumented(node)); + } + + private shouldLocationBeDocumented(node: ts.Node) { + if (this.locations.has(ALL)) { + return true; + } + + if (Lint.hasModifier(node.modifiers, ts.SyntaxKind.StaticKeyword)) { + return this.locations.has(LOCATION_STATIC); + } + + return this.locations.has(LOCATION_INSTANCE); + } + + private shouldPrivacyBeDocumented(node: ts.Node) { + if (this.privacies.has(ALL)) { + return true; + } + + if (Lint.hasModifier(node.modifiers, ts.SyntaxKind.PrivateKeyword)) { + return this.privacies.has(PRIVACY_PRIVATE); + } + + if (Lint.hasModifier(node.modifiers, ts.SyntaxKind.ProtectedKeyword)) { + return this.privacies.has(PRIVACY_PROTECTED); + } + + return this.privacies.has(PRIVACY_PUBLIC); + } +} diff --git a/src/rules/completed-docs/exclusion.ts b/src/rules/completed-docs/exclusion.ts new file mode 100644 index 00000000000..19a45c1c453 --- /dev/null +++ b/src/rules/completed-docs/exclusion.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright 2013 Palantir Technologies, Inc. + * + * 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 + * + * http://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. + */ + +import * as ts from "typescript"; + +import { All, ALL } from "../completedDocsRule"; +import { ExclusionDescriptor } from "./exclusionDescriptors"; + +export abstract class Exclusion { + public constructor(protected readonly descriptor: Partial = {}) { } + + public abstract excludes(node: ts.Node): boolean; + + protected createSet(values?: T[]): Set { + if (values === undefined || values.length === 0) { + values = [ALL as T]; + } + + return new Set(values); + } +} diff --git a/src/rules/completed-docs/exclusionDescriptors.ts b/src/rules/completed-docs/exclusionDescriptors.ts new file mode 100644 index 00000000000..c3654584bbd --- /dev/null +++ b/src/rules/completed-docs/exclusionDescriptors.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2013 Palantir Technologies, Inc. + * + * 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 + * + * http://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. + */ + +import { DocType } from "../completedDocsRule"; +import { IBlockExclusionDescriptor } from "./blockExclusion"; +import { IClassExclusionDescriptor } from "./classExclusion"; +import { ITagExclusionDescriptor } from "./tagExclusion"; + +export type ExclusionDescriptor = IBlockExclusionDescriptor | IClassExclusionDescriptor | ITagExclusionDescriptor; + +export type InputExclusionDescriptor = boolean | ExclusionDescriptor; + +export interface IExclusionDescriptors { + [type: string /* DocType */]: ExclusionDescriptor; +} + +export type IInputExclusionDescriptors = DocType | { + [type: string /* DocType */]: InputExclusionDescriptor; +}; diff --git a/src/rules/completed-docs/exclusionFactory.ts b/src/rules/completed-docs/exclusionFactory.ts new file mode 100644 index 00000000000..bbd6e7d5d17 --- /dev/null +++ b/src/rules/completed-docs/exclusionFactory.ts @@ -0,0 +1,65 @@ +/** + * @license + * Copyright 2013 Palantir Technologies, Inc. + * + * 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 + * + * http://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. + */ + +import { hasOwnProperty } from "../../utils"; +import { DocType } from "../completedDocsRule"; +import { BlockExclusion, IBlockExclusionDescriptor } from "./blockExclusion"; +import { ClassExclusion, IClassExclusionDescriptor } from "./classExclusion"; +import { Exclusion } from "./exclusion"; +import { IInputExclusionDescriptors, InputExclusionDescriptor } from "./exclusionDescriptors"; +import { ITagExclusionDescriptor, TagExclusion } from "./tagExclusion"; + +export class ExclusionFactory { + public constructExclusionsMap(ruleArguments: IInputExclusionDescriptors[]): Map>> { + const exclusionsMap: Map>> = new Map(); + + for (const ruleArgument of ruleArguments) { + this.addRequirements(exclusionsMap, ruleArgument); + } + + return exclusionsMap; + } + + private addRequirements(exclusionsMap: Map>>, descriptors: IInputExclusionDescriptors) { + if (typeof descriptors === "string") { + exclusionsMap.set(descriptors, this.createRequirementsForDocType(descriptors, {})); + return; + } + + for (const docType in descriptors) { + if (hasOwnProperty(descriptors, docType)) { + exclusionsMap.set(docType as DocType, this.createRequirementsForDocType(docType as DocType, descriptors[docType])); + } + } + } + + private createRequirementsForDocType(docType: DocType, descriptor: InputExclusionDescriptor) { + const requirements = []; + + if (docType === "methods" || docType === "properties") { + requirements.push(new ClassExclusion(descriptor as IClassExclusionDescriptor)); + } else { + requirements.push(new BlockExclusion(descriptor as IBlockExclusionDescriptor)); + } + + if ((descriptor as ITagExclusionDescriptor).tags !== undefined) { + requirements.push(new TagExclusion(descriptor as ITagExclusionDescriptor)); + } + + return requirements; + } +} diff --git a/src/rules/completed-docs/tagExclusion.ts b/src/rules/completed-docs/tagExclusion.ts new file mode 100644 index 00000000000..a394724b163 --- /dev/null +++ b/src/rules/completed-docs/tagExclusion.ts @@ -0,0 +1,99 @@ +/** + * @license + * Copyright 2013 Palantir Technologies, Inc. + * + * 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 + * + * http://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. + */ + +import * as ts from "typescript"; + +import { Exclusion } from "./exclusion"; + +export interface ITagExclusionDescriptor { + tags?: { + content: IContentTags; + existence: string[]; + }; +} + +export interface IContentTags { + [i: string]: string; +} + +export class TagExclusion extends Exclusion { + private readonly contentTags: IContentTags = this.descriptor.tags === undefined + ? {} + : this.descriptor.tags.content; + + private readonly existenceTags = new Set( + this.descriptor.tags !== undefined && this.descriptor.tags.existence !== undefined + ? this.descriptor.tags.existence + : undefined); + + public excludes(node: ts.Node) { + const documentationNode = this.getDocumentationNode(node); + const tagsWithContents = this.parseTagsWithContents(documentationNode.getFullText()); + + for (const tagWithContent of tagsWithContents) { + if (this.existenceTags.has(tagWithContent[0])) { + return true; + } + + const matcherBody = this.contentTags[tagWithContent[0]]; + if (matcherBody === undefined) { + continue; + } + + if (new RegExp(matcherBody).test(tagWithContent[1])) { + return true; + } + } + + return false; + } + + private getDocumentationNode(node: ts.Node) { + if (node.kind === ts.SyntaxKind.VariableDeclaration) { + return node.parent!; + } + + return node; + } + + private parseTagsWithContents(nodeText: string | undefined): Array<[string, string]> { + if (nodeText === undefined) { + return []; + } + + const docMatches = nodeText.match((/\/\*\*\s*\n([^\*]*(\*[^\/])?)*\*\//)); + if (docMatches === null || docMatches.length === 0) { + return []; + } + + const lines = docMatches[0].match(/[\r\n\s]*\*\s*@.*[\r\n\s]/g); + if (lines === null) { + return []; + } + + return lines + .map((line): [string, string] => { + const body = line.substring(line.indexOf("@")); + const firstSpaceIndex = body.search(/\s/); + + return [ + body.substring(1, firstSpaceIndex), + body.substring(firstSpaceIndex).trim(), + ]; + }); + } +} diff --git a/src/rules/completedDocsRule.ts b/src/rules/completedDocsRule.ts index a4145f12acf..6ec1640da29 100644 --- a/src/rules/completedDocsRule.ts +++ b/src/rules/completedDocsRule.ts @@ -19,22 +19,9 @@ import { isVariableDeclarationList, isVariableStatement } from "tsutils"; import * as ts from "typescript"; import * as Lint from "../index"; -import { hasOwnProperty } from "../utils"; - -export interface IBlockRequirementDescriptor { - visibilities?: Visibility[]; -} - -export interface IClassRequirementDescriptor { - locations?: Location[]; - privacies?: Privacy[]; -} - -export type RequirementDescriptor = IBlockRequirementDescriptor | IClassRequirementDescriptor; - -export interface IRequirementDescriptors { - [type: string /* DocType */]: RequirementDescriptor; -} +import { Exclusion } from "./completed-docs/exclusion"; +import { IInputExclusionDescriptors } from "./completed-docs/exclusionDescriptors"; +import { ExclusionFactory } from "./completed-docs/exclusionFactory"; export const ALL = "all"; @@ -49,6 +36,7 @@ export const ARGUMENT_PROPERTIES = "properties"; export const ARGUMENT_TYPES = "types"; export const ARGUMENT_VARIABLES = "variables"; +export const DESCRIPTOR_TAGS = "tags"; export const DESCRIPTOR_LOCATIONS = "locations"; export const DESCRIPTOR_PRIVACIES = "privacies"; export const DESCRIPTOR_VISIBILITIES = "visibilities"; @@ -60,6 +48,9 @@ export const PRIVACY_PRIVATE = "private"; export const PRIVACY_PROTECTED = "protected"; export const PRIVACY_PUBLIC = "public"; +export const TAGS_FOR_CONTENT = "content"; +export const TAGS_FOR_EXISTENCE = "exists"; + export const VISIBILITY_EXPORTED = "exported"; export const VISIBILITY_INTERNAL = "internal"; @@ -90,20 +81,54 @@ export type Visibility = All | typeof VISIBILITY_EXPORTED | typeof VISIBILITY_INTERNAL; -type BlockOrClassRequirement = BlockRequirement | ClassRequirement; - export class Rule extends Lint.Rules.TypedRule { public static FAILURE_STRING_EXIST = "Documentation must exist for "; - public static defaultArguments = [ - ARGUMENT_CLASSES, - ARGUMENT_FUNCTIONS, - ARGUMENT_METHODS, - ARGUMENT_PROPERTIES, - ] as DocType[]; + public static defaultArguments: IInputExclusionDescriptors = { + [ARGUMENT_CLASSES]: true, + [ARGUMENT_FUNCTIONS]: true, + [ARGUMENT_METHODS]: { + [DESCRIPTOR_TAGS]: { + [TAGS_FOR_CONTENT]: { + see: ".*", + }, + [TAGS_FOR_EXISTENCE]: [ + "deprecated", + "inheritdoc", + ], + }, + }, + [ARGUMENT_PROPERTIES]: { + [DESCRIPTOR_TAGS]: { + [TAGS_FOR_CONTENT]: { + see: ".*", + }, + [TAGS_FOR_EXISTENCE]: [ + "deprecated", + "inheritdoc", + ], + }, + }, + }; public static ARGUMENT_DESCRIPTOR_BLOCK = { properties: { + [DESCRIPTOR_TAGS]: { + properties: { + [TAGS_FOR_CONTENT]: { + items: { + type: "string", + }, + type: "object", + }, + [TAGS_FOR_EXISTENCE]: { + items: { + type: "string", + }, + type: "array", + }, + }, + }, [DESCRIPTOR_VISIBILITIES]: { enum: [ ALL, @@ -118,6 +143,22 @@ export class Rule extends Lint.Rules.TypedRule { public static ARGUMENT_DESCRIPTOR_CLASS = { properties: { + [DESCRIPTOR_TAGS]: { + properties: { + [TAGS_FOR_CONTENT]: { + items: { + type: "string", + }, + type: "object", + }, + [TAGS_FOR_EXISTENCE]: { + items: { + type: "string", + }, + type: "array", + }, + }, + }, [DESCRIPTOR_LOCATIONS]: { enum: [ ALL, @@ -144,7 +185,7 @@ export class Rule extends Lint.Rules.TypedRule { ruleName: "completed-docs", description: "Enforces documentation for important items be filled out.", optionsDescription: Lint.Utils.dedent` - \`true\` to enable for ["${ARGUMENT_CLASSES}", "${ARGUMENT_FUNCTIONS}", "${ARGUMENT_METHODS}", "${ARGUMENT_PROPERTIES}"], + \`true\` to enable for [${Object.keys(Rule.defaultArguments).join(", ")}]], or an array with each item in one of two formats: * \`string\` to enable for that type @@ -159,10 +200,14 @@ export class Rule extends Lint.Rules.TypedRule { * \`"${ALL}"\` * \`"${LOCATION_INSTANCE}"\` * \`"${LOCATION_STATIC}"\` - * All other types may specify \`"${DESCRIPTOR_VISIBILITIES}"\`: + * Other types may specify \`"${DESCRIPTOR_VISIBILITIES}"\`: * \`"${ALL}"\` * \`"${VISIBILITY_EXPORTED}"\` * \`"${VISIBILITY_INTERNAL}"\` + * All types may also provide \`"${DESCRIPTOR_TAGS}"\` + with members specifying tags that allow the docs to not have a body. + * \`"${TAGS_FOR_CONTENT}"\`: Object mapping tags to \`RegExp\` bodies content allowed to count as complete docs. + * \`"${TAGS_FOR_EXISTENCE}"\`: Array of tags that must only exist to count as complete docs. Types that may be enabled are: @@ -181,7 +226,17 @@ export class Rule extends Lint.Rules.TypedRule { items: { anyOf: [ { - enum: Rule.defaultArguments, + options: [ + ARGUMENT_CLASSES, + ARGUMENT_ENUMS, + ARGUMENT_FUNCTIONS, + ARGUMENT_INTERFACES, + ARGUMENT_METHODS, + ARGUMENT_NAMESPACES, + ARGUMENT_PROPERTIES, + ARGUMENT_TYPES, + ARGUMENT_VARIABLES, + ], type: "string", }, { @@ -216,125 +271,39 @@ export class Rule extends Lint.Rules.TypedRule { [DESCRIPTOR_LOCATIONS]: LOCATION_INSTANCE, [DESCRIPTOR_PRIVACIES]: [PRIVACY_PUBLIC, PRIVACY_PROTECTED], }, + [DESCRIPTOR_TAGS]: { + [TAGS_FOR_CONTENT]: { + see: ["#.*"], + }, + [TAGS_FOR_EXISTENCE]: ["inheritdoc"], + }, }, ], ], + type: "style", typescriptOnly: false, requiresTypeInfo: true, }; /* tslint:enable:object-literal-sort-keys */ + private readonly exclusionFactory = new ExclusionFactory(); + public applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { const options = this.getOptions(); const completedDocsWalker = new CompletedDocsWalker(sourceFile, options, program); - completedDocsWalker.setRequirements(this.getRequirements(options.ruleArguments)); + completedDocsWalker.setExclusionsMap(this.getExclusionsMap(options.ruleArguments)); return this.applyWithWalker(completedDocsWalker); } - private getRequirements(ruleArguments: Array): Map { + private getExclusionsMap(ruleArguments: Array): Map>> { if (ruleArguments.length === 0) { - ruleArguments = Rule.defaultArguments; - } - - return Requirement.constructRequirements(ruleArguments); - } -} - -abstract class Requirement { - public static constructRequirements(ruleArguments: Array): Map { - const requirements: Map = new Map(); - - for (const ruleArgument of ruleArguments) { - Requirement.addRequirements(requirements, ruleArgument); - } - - return requirements; - } - - private static addRequirements(requirements: Map, descriptor: DocType | IRequirementDescriptors) { - if (typeof descriptor === "string") { - requirements.set(descriptor, new BlockRequirement()); - return; - } - - for (const type in descriptor) { - if (hasOwnProperty(descriptor, type)) { - requirements.set( - type as DocType, - (type === "methods" || type === "properties") - ? new ClassRequirement(descriptor[type] as IClassRequirementDescriptor) - : new BlockRequirement(descriptor[type] as IBlockRequirementDescriptor)); - } - } - } - - // tslint:disable-next-line no-object-literal-type-assertion - protected constructor(public readonly descriptor: TDescriptor = {} as TDescriptor) { } - - public abstract shouldNodeBeDocumented(node: ts.Declaration): boolean; - - protected createSet(values?: T[]): Set { - if (values === undefined || values.length === 0) { - values = [ALL as T]; - } - - return new Set(values); - } -} - -class BlockRequirement extends Requirement { - public readonly visibilities: Set = this.createSet(this.descriptor.visibilities); - - public shouldNodeBeDocumented(node: ts.Node): boolean { - if (this.visibilities.has(ALL)) { - return true; - } - - if (Lint.hasModifier(node.modifiers, ts.SyntaxKind.ExportKeyword)) { - return this.visibilities.has(VISIBILITY_EXPORTED); - } - - return this.visibilities.has(VISIBILITY_INTERNAL); - } -} - -class ClassRequirement extends Requirement { - public readonly locations: Set = this.createSet(this.descriptor.locations); - public readonly privacies: Set = this.createSet(this.descriptor.privacies); - - public shouldNodeBeDocumented(node: ts.Node) { - return this.shouldLocationBeDocumented(node) && this.shouldPrivacyBeDocumented(node); - } - - private shouldLocationBeDocumented(node: ts.Node) { - if (this.locations.has(ALL)) { - return true; - } - - if (Lint.hasModifier(node.modifiers, ts.SyntaxKind.StaticKeyword)) { - return this.locations.has(LOCATION_STATIC); - } - - return this.locations.has(LOCATION_INSTANCE); - } - - private shouldPrivacyBeDocumented(node: ts.Node) { - if (this.privacies.has(ALL)) { - return true; - } - - if (Lint.hasModifier(node.modifiers, ts.SyntaxKind.PrivateKeyword)) { - return this.privacies.has(PRIVACY_PRIVATE); - } - - if (Lint.hasModifier(node.modifiers, ts.SyntaxKind.ProtectedKeyword)) { - return this.privacies.has(PRIVACY_PROTECTED); + ruleArguments = [Rule.defaultArguments]; } - return this.privacies.has(PRIVACY_PUBLIC); + return this.exclusionFactory.constructExclusionsMap(ruleArguments); } } @@ -343,10 +312,10 @@ class CompletedDocsWalker extends Lint.ProgramAwareRuleWalker { export: "exported", }; - private requirements: Map; + private exclusionsMap: Map>>; - public setRequirements(requirements: Map): void { - this.requirements = requirements; + public setExclusionsMap(exclusionsMap: Map>>): void { + this.exclusionsMap = exclusionsMap; } public visitClassDeclaration(node: ts.ClassDeclaration): void { @@ -401,6 +370,32 @@ class CompletedDocsWalker extends Lint.ProgramAwareRuleWalker { super.visitVariableDeclaration(node); } + private checkNode(node: ts.NamedDeclaration, nodeType: DocType, requirementNode: ts.Node = node): void { + const { name } = node; + if (name === undefined) { + return; + } + + const exclusions = this.exclusionsMap.get(nodeType); + if (exclusions === undefined) { + return; + } + + for (const exclusion of exclusions) { + if (exclusion.excludes(requirementNode)) { + return; + } + } + + const symbol = this.getTypeChecker().getSymbolAtLocation(name); + if (symbol === undefined) { + return; + } + + const comments = symbol.getDocumentationComment(); + this.checkComments(node, this.describeNode(nodeType), comments, requirementNode); + } + private checkVariable(node: ts.VariableDeclaration) { // Only check variables in variable declaration lists // and not variables in catch clauses and for loops. @@ -423,37 +418,13 @@ class CompletedDocsWalker extends Lint.ProgramAwareRuleWalker { } } - private checkNode(node: ts.NamedDeclaration, nodeType: DocType, requirementNode: ts.Node = node): void { - const { name } = node; - if (name === undefined) { - return; - } - - const requirement = this.requirements.get(nodeType); - if (requirement === undefined || !requirement.shouldNodeBeDocumented(requirementNode)) { - return; - } - - const symbol = this.getTypeChecker().getSymbolAtLocation(name); - if (symbol === undefined) { - return; - } - - const comments = symbol.getDocumentationComment(); - this.checkComments(node, this.describeNode(nodeType), comments, requirementNode); - } - - private describeNode(nodeType: DocType): string { - return nodeType.replace("-", " "); - } - - private checkComments(node: ts.Declaration, nodeDescriptor: string, comments: ts.SymbolDisplayPart[], requirementNode: ts.Node) { + private checkComments(node: ts.Node, nodeDescriptor: string, comments: ts.SymbolDisplayPart[], requirementNode: ts.Node) { if (comments.map((comment: ts.SymbolDisplayPart) => comment.text).join("").trim() === "") { this.addDocumentationFailure(node, nodeDescriptor, requirementNode); } } - private addDocumentationFailure(node: ts.Declaration, nodeType: string, requirementNode: ts.Node): void { + private addDocumentationFailure(node: ts.Node, nodeType: string, requirementNode: ts.Node): void { const start = node.getStart(); const width = node.getText().split(/\r|\n/g)[0].length; const description = this.describeDocumentationFailure(requirementNode, nodeType); @@ -476,4 +447,8 @@ class CompletedDocsWalker extends Lint.ProgramAwareRuleWalker { const alias = CompletedDocsWalker.modifierAliases[description]; return alias !== undefined ? alias : description; } + + private describeNode(nodeType: DocType): string { + return nodeType.replace("-", " "); + } } diff --git a/test/rules/completed-docs/tags/content/test.ts.lint b/test/rules/completed-docs/tags/content/test.ts.lint new file mode 100644 index 00000000000..4d381365478 --- /dev/null +++ b/test/rules/completed-docs/tags/content/test.ts.lint @@ -0,0 +1,30 @@ +const CompletelyEmptyVariable = 0; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for variables.] + +/** + * + */ +const ContentEmptyVariable = 1; + ~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for variables.] + +/** + * @see + */ +const ContentMissingVariable = 2; + ~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for variables.] + +/** + * @see wat + */ +const ContentInvalidVariable = 3; + ~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for variables.] + +/** + * @see #123 + */ +const ContentValidVariable = 4; + +/** + * ... + */ +const CommentBodyVariableRofl = 5; diff --git a/test/rules/completed-docs/tags/content/tsconfig.json b/test/rules/completed-docs/tags/content/tsconfig.json new file mode 100644 index 00000000000..744a66c893a --- /dev/null +++ b/test/rules/completed-docs/tags/content/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "module": "commonjs" + } +} diff --git a/test/rules/completed-docs/tags/content/tslint.json b/test/rules/completed-docs/tags/content/tslint.json new file mode 100644 index 00000000000..2244e13529f --- /dev/null +++ b/test/rules/completed-docs/tags/content/tslint.json @@ -0,0 +1,19 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "completed-docs": [ + true, + { + "variables": { + "tags": { + "content": { + "see": "^(\\s*)#(\\d+)(\\s*)$" + } + } + } + } + ] + } +} diff --git a/test/rules/completed-docs/tags/existence/test.ts.lint b/test/rules/completed-docs/tags/existence/test.ts.lint new file mode 100644 index 00000000000..924bd17fc4d --- /dev/null +++ b/test/rules/completed-docs/tags/existence/test.ts.lint @@ -0,0 +1,23 @@ +const CompletelyEmptyVariable = 0; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for variables.] + +/** + * + */ +const ContentEmptyVariable = 1; + ~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for variables.] + +/** + * @deprecated + */ +const ContentMissingVariable = 2; + +/** + * @deprecated Do not use! + */ +const ContentValidVariable = 4; + +/** + * ... + */ +const CommentBodyVariable = 5; diff --git a/test/rules/completed-docs/tags/existence/tsconfig.json b/test/rules/completed-docs/tags/existence/tsconfig.json new file mode 100644 index 00000000000..744a66c893a --- /dev/null +++ b/test/rules/completed-docs/tags/existence/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "module": "commonjs" + } +} diff --git a/test/rules/completed-docs/tags/existence/tslint.json b/test/rules/completed-docs/tags/existence/tslint.json new file mode 100644 index 00000000000..772b3d0a4d7 --- /dev/null +++ b/test/rules/completed-docs/tags/existence/tslint.json @@ -0,0 +1,17 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "completed-docs": [ + true, + { + "variables": { + "tags": { + "existence": ["deprecated"] + } + } + } + ] + } +}