Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to warn on undocumented items #1819

Merged
merged 11 commits into from Jan 23, 2022
4 changes: 4 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,9 @@
# Unreleased

### Features

- Added `--validation.ensureDocumented` option to warn on items that are not documented
Nokel81 marked this conversation as resolved.
Show resolved Hide resolved

### Bug Fixes

- Fixed line height of `h1` and `h2` elements being too low, #1796.
Expand Down
9 changes: 9 additions & 0 deletions src/lib/application.ts
Expand Up @@ -34,6 +34,7 @@ import {
} from "./utils/entry-point";
import { nicePath } from "./utils/paths";
import { hasBeenLoadedMultipleTimes } from "./utils/general";
import { validateDocumentation } from "./validation/documentation";

// eslint-disable-next-line @typescript-eslint/no-var-requires
const packageInfo = require("../../package.json") as {
Expand Down Expand Up @@ -416,6 +417,14 @@ export class Application extends ChildableComponent<
);
}

if (checks.notDocumented) {
validateDocumentation(
project,
this.logger,
this.options.getValue("requiredToBeDocumented")
);
}

// checks.invalidLink is currently handled when rendering by the MarkedLinksPlugin.
// It should really move here, but I'm putting that off until done refactoring the comment
// parsing so that we don't have duplicate parse logic all over the place.
Expand Down
6 changes: 6 additions & 0 deletions src/lib/utils/options/declaration.ts
Expand Up @@ -3,6 +3,7 @@ import type { LogLevel } from "../loggers";
import type { SortStrategy } from "../sort";
import { isAbsolute, join, resolve } from "path";
import type { EntryPointStrategy } from "../entry-point";
import type { ReflectionKind } from "../../models";

export const EmitStrategy = {
true: true, // Alias for both, for backwards compatibility until 0.23
Expand Down Expand Up @@ -115,6 +116,7 @@ export interface TypeDocOptionMap {
/** @deprecated use validation.invalidLink */
listInvalidSymbolLinks: boolean;
validation: ValidationOptions;
requiredToBeDocumented: (keyof typeof ReflectionKind)[];
}

export type ValidationOptions = {
Expand All @@ -127,6 +129,10 @@ export type ValidationOptions = {
* If set, TypeDoc will produce warnings about \{&amp;link\} tags which will produce broken links.
*/
invalidLink: boolean;
/**
* If set, TypeDoc will produce warnings about declarations that do not have doc comments
*/
notDocumented: boolean;
};

/**
Expand Down
115 changes: 90 additions & 25 deletions src/lib/utils/options/sources/typedoc.ts
Expand Up @@ -4,19 +4,23 @@ import { ParameterType, ParameterHint, EmitStrategy } from "../declaration";
import { BUNDLED_THEMES, Theme } from "shiki";
import { SORT_STRATEGIES } from "../../sort";
import { EntryPointStrategy } from "../../entry-point";
import { ReflectionKind } from "../../../models";
import { toOrdinal } from "../../ordinal-numbers";

export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
options.addDeclaration({
type: ParameterType.Path,
name: "options",
help: "Specify a json option file that should be loaded. If not specified TypeDoc will look for 'typedoc.json' in the current directory.",
help:
"Specify a json option file that should be loaded. If not specified TypeDoc will look for 'typedoc.json' in the current directory.",
hint: ParameterHint.File,
defaultValue: process.cwd(),
});
options.addDeclaration({
type: ParameterType.Path,
name: "tsconfig",
help: "Specify a TypeScript config file that should be loaded. If not specified TypeDoc will look for 'tsconfig.json' in the current directory.",
help:
"Specify a TypeScript config file that should be loaded. If not specified TypeDoc will look for 'tsconfig.json' in the current directory.",
hint: ParameterHint.File,
defaultValue: process.cwd(),
});
Expand All @@ -27,20 +31,23 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
});
options.addDeclaration({
name: "entryPointStrategy",
help: "The strategy to be used to convert entry points into documentation modules.",
help:
"The strategy to be used to convert entry points into documentation modules.",
type: ParameterType.Map,
map: EntryPointStrategy,
defaultValue: EntryPointStrategy.Resolve,
});

options.addDeclaration({
name: "exclude",
help: "Define patterns to be excluded when expanding a directory that was specified as an entry point.",
help:
"Define patterns to be excluded when expanding a directory that was specified as an entry point.",
type: ParameterType.GlobArray,
});
options.addDeclaration({
name: "externalPattern",
help: "Define patterns for files that should be considered being external.",
help:
"Define patterns for files that should be considered being external.",
type: ParameterType.GlobArray,
defaultValue: ["**/node_modules/**"],
});
Expand All @@ -51,12 +58,14 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
});
options.addDeclaration({
name: "excludeNotDocumented",
help: "Prevent symbols that are not explicitly documented from appearing in the results.",
help:
"Prevent symbols that are not explicitly documented from appearing in the results.",
type: ParameterType.Boolean,
});
options.addDeclaration({
name: "excludeInternal",
help: "Prevent symbols that are marked with @internal from being documented.",
help:
"Prevent symbols that are marked with @internal from being documented.",
type: ParameterType.Boolean,
});
options.addDeclaration({
Expand All @@ -76,13 +85,15 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
});
options.addDeclaration({
name: "includes",
help: "Specify the location to look for included documents (use [[include:FILENAME]] in comments).",
help:
"Specify the location to look for included documents (use [[include:FILENAME]] in comments).",
type: ParameterType.Path,
hint: ParameterHint.Directory,
});
options.addDeclaration({
name: "media",
help: "Specify the location with media files that should be copied to the output directory.",
help:
"Specify the location with media files that should be copied to the output directory.",
type: ParameterType.Path,
hint: ParameterHint.Directory,
});
Expand All @@ -94,7 +105,8 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
});
options.addDeclaration({
name: "preserveWatchOutput",
help: "If set, TypeDoc will not clear the screen between compilation runs.",
help:
"If set, TypeDoc will not clear the screen between compilation runs.",
type: ParameterType.Boolean,
});
options.addDeclaration({
Expand All @@ -113,7 +125,8 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
});
options.addDeclaration({
name: "json",
help: "Specify the location and filename a JSON file describing the project is written to.",
help:
"Specify the location and filename a JSON file describing the project is written to.",
type: ParameterType.Path,
hint: ParameterHint.File,
});
Expand Down Expand Up @@ -175,7 +188,8 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {

options.addDeclaration({
name: "name",
help: "Set the name of the project that will be used in the header of the template.",
help:
"Set the name of the project that will be used in the header of the template.",
});
options.addDeclaration({
name: "includeVersion",
Expand All @@ -189,17 +203,20 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
});
options.addDeclaration({
name: "readme",
help: "Path to the readme file that should be displayed on the index page. Pass `none` to disable the index page and start the documentation on the globals page.",
help:
"Path to the readme file that should be displayed on the index page. Pass `none` to disable the index page and start the documentation on the globals page.",
type: ParameterType.Path,
});
options.addDeclaration({
name: "defaultCategory",
help: "Specify the default category for reflections without a category.",
help:
"Specify the default category for reflections without a category.",
defaultValue: "Other",
});
options.addDeclaration({
name: "categoryOrder",
help: "Specify the order in which categories appear. * indicates the relative order for categories not in the list.",
help:
"Specify the order in which categories appear. * indicates the relative order for categories not in the list.",
type: ParameterType.Array,
});
options.addDeclaration({
Expand Down Expand Up @@ -234,16 +251,19 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
});
options.addDeclaration({
name: "gitRevision",
help: "Use specified revision instead of the last revision for linking to GitHub/Bitbucket source files.",
help:
"Use specified revision instead of the last revision for linking to GitHub/Bitbucket source files.",
});
options.addDeclaration({
name: "gitRemote",
help: "Use the specified remote for linking to GitHub/Bitbucket source files.",
help:
"Use the specified remote for linking to GitHub/Bitbucket source files.",
defaultValue: "origin",
});
options.addDeclaration({
name: "gaID",
help: "Set the Google Analytics tracking ID and activate tracking code.",
help:
"Set the Google Analytics tracking ID and activate tracking code.",
});
options.addDeclaration({
name: "gaSite",
Expand All @@ -252,7 +272,8 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
});
options.addDeclaration({
name: "githubPages",
help: "Generate a .nojekyll file to prevent 404 errors in GitHub Pages. Defaults to `true`.",
help:
"Generate a .nojekyll file to prevent 404 errors in GitHub Pages. Defaults to `true`.",
type: ParameterType.Boolean,
defaultValue: true,
});
Expand All @@ -268,7 +289,8 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
});
options.addDeclaration({
name: "cleanOutputDir",
help: "If set, TypeDoc will remove the output directory before writing output.",
help:
"If set, TypeDoc will remove the output directory before writing output.",
type: ParameterType.Boolean,
defaultValue: true,
});
Expand All @@ -290,7 +312,8 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
});
options.addDeclaration({
name: "plugin",
help: "Specify the npm plugins that should be loaded. Omit to load all installed plugins, set to 'none' to load no plugins.",
help:
"Specify the npm plugins that should be loaded. Omit to load all installed plugins, set to 'none' to load no plugins.",
type: ParameterType.ModuleArray,
});
options.addDeclaration({
Expand All @@ -308,7 +331,8 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
});
options.addDeclaration({
name: "markedOptions",
help: "Specify the options passed to Marked, the Markdown parser used by TypeDoc.",
help:
"Specify the options passed to Marked, the Markdown parser used by TypeDoc.",
type: ParameterType.Mixed,
validate(value) {
if (
Expand All @@ -330,22 +354,63 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
});
options.addDeclaration({
name: "listInvalidSymbolLinks",
help: "Emit a list of broken symbol {@link navigation} links after documentation generation, DEPRECATED, prefer validation.invalidLink instead.",
help:
"Emit a list of broken symbol {@link navigation} links after documentation generation, DEPRECATED, prefer validation.invalidLink instead.",
type: ParameterType.Boolean,
});
options.addDeclaration({
name: "intentionallyNotExported",
help: "A list of types which should not produce 'referenced but not documented' warnings.",
help:
"A list of types which should not produce 'referenced but not documented' warnings.",
type: ParameterType.Array,
});
options.addDeclaration({
name: "requiredToBeDocumented",
help: "A list of reflection kinds that must be documented",
type: ParameterType.Array,
validate(values) {
const validValues = Object.values(ReflectionKind)
// this is good enough because the values of the ReflectionKind enum are all numbers
.filter((v) => typeof v === "string")
.join(", ");
for (
let i = 0, kind = values[i];
i < values.length;
i += 1, kind = values[i]
) {
if (!(kind in ReflectionKind)) {
throw new Error(
`The ${toOrdinal(
i + 1
)} 'requiredToBeDocumented' value is invalid. Must be one of: ${validValues}`
);
}
}
},
defaultValue: [
"Enum",
"EnumMember",
"Variable",
"Function",
"Class",
"Interface",
"Property",
"Method",
"GetSignature",
"SetSignature",
"TypeAlias",
],
});

options.addDeclaration({
name: "validation",
help: "Specify which validation steps TypeDoc should perform on your generated documentation.",
help:
"Specify which validation steps TypeDoc should perform on your generated documentation.",
type: ParameterType.Flags,
defaults: {
notExported: true,
invalidLink: false,
notDocumented: false,
},
});
}
25 changes: 25 additions & 0 deletions src/lib/utils/ordinal-numbers.ts
@@ -0,0 +1,25 @@
/**
* Format an integer value as an ordinal string. Throwing if the value is not a
* positive integer
* @param value The integer value to format as its ordinal version
*/
export function toOrdinal(value: number): string {
if (!Number.isInteger(value)) {
throw new TypeError("value must be an integer number");
}

if (value < 0) {
throw new TypeError("value must be a positive integer");
}

const onesDigit = value % 10;
const tensDigit = ((value % 100) - onesDigit) / 10;

if (tensDigit === 1) {
return `${value}th`;
}

const ordinal = onesDigit === 1 ? "st" : onesDigit === 2 ? "nd" : "th";

return `${value}${ordinal}`;
}
38 changes: 38 additions & 0 deletions src/lib/validation/documentation.ts
@@ -0,0 +1,38 @@
import * as path from "path";
import * as ts from "typescript";
import { ProjectReflection, ReflectionKind } from "../models";
import { Logger, normalizePath } from "../utils";

export function validateDocumentation(
project: ProjectReflection,
logger: Logger,
requiredToBeDocumented: readonly (keyof typeof ReflectionKind)[]
): void {
const kinds = requiredToBeDocumented.reduce(
(prev, cur) => (prev |= ReflectionKind[cur]),
0
);

for (const ref of project.getReflectionsByKind(kinds)) {
const symbol = project.getSymbolFromReflection(ref);
if (!ref.comment && symbol?.declarations) {
const decl = symbol.declarations[0];
const sourceFile = decl.getSourceFile();
const { line } = ts.getLineAndCharacterOfPosition(
Nokel81 marked this conversation as resolved.
Show resolved Hide resolved
sourceFile,
decl.getStart()
);
const file = normalizePath(
path.relative(process.cwd(), sourceFile.fileName)
);

if (file.startsWith(`node_modules${path.sep}`)) {
Nokel81 marked this conversation as resolved.
Show resolved Hide resolved
continue;
}

logger.warn(
`${ref.name}, defined at ${file}:${line+1}, does not have any documentation.`
);
}
}
}