Skip to content
Draft
Show file tree
Hide file tree
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
77 changes: 51 additions & 26 deletions packages/cli/src/commands/check/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import { Command } from "clipanion";
import { isEmpty } from "es-toolkit/compat";
import path from "path";
import { loadConfig } from "../../config/load-config.js";
import { createPackageManager } from "../../package-manager/create-package-manager.js";
import { isTargetPackage } from "../../package-manager/utils/is-target-package.js";
import { getPackageEntryPoints } from "../../core/entry-point.js";
import { getTsConfigPath } from "../../core/get-ts-config-path.js";
import { getTsProject } from "../../core/get-ts-project.js";
import { isExportSourceFile } from "../../core/parser/source/is-export-source-file.js";
import { getExportedDeclarationsBySourceFile } from "../../core/parser/source/get-exported-declarations-by-sourcefile.js";
import { excludeBarrelReExports } from "../../core/parser/source/exclude-barrel-re-exports.js";
import { hasJSDocTag } from "../../core/parser/jsdoc/jsdoc-utils.js";
import { getPackageEntryPoints } from "../../core/entry-point.js";
import path from "path";
import { getExportedDeclarationsBySourceFile } from "../../core/parser/source/get-exported-declarations-by-sourcefile.js";
import { isExportSourceFile } from "../../core/parser/source/is-export-source-file.js";
import { createPackageManager } from "../../package-manager/create-package-manager.js";
import { isTargetPackage } from "../../package-manager/utils/is-target-package.js";
import {
PackageValidationResult,
validateExports,
} from "./validate/validate-exports.js";
import { ValidationError } from "./validate/validate.types.js";

export class CheckCommand extends Command {
static paths = [[`check`]];

async execute(): Promise<number> {
const { checkConfig, projectConfig, targetPackages } = await loadContext();

if (targetPackages.length === 0) {
if (isEmpty(targetPackages)) {
printNoPackagesFound(
projectConfig.workspace.include,
projectConfig.root,
Expand All @@ -26,6 +31,7 @@ export class CheckCommand extends Command {
return 1;
}

let hasErrors = false;
for (const pkg of targetPackages) {
console.log(`📝 ${pkg.name} processing...`);

Expand All @@ -46,28 +52,19 @@ export class CheckCommand extends Command {
const exportDeclarationsBySourceFiles = exportSourceFiles.flatMap(
getExportedDeclarationsBySourceFile
);
const excludeBarrelReExport = excludeBarrelReExports(
exportDeclarationsBySourceFiles
);
const missingJSDocExports = excludeBarrelReExport.filter((target) => {
return !hasJSDocTag(target.declaration, "public");
});
const exportDeclarations = excludeBarrelReExports(exportDeclarationsBySourceFiles);
const result = validateExports(exportDeclarations, projectConfig.root);
if (!isEmpty(result.issues)) {
printValidationErrors(pkg.name, result);
hasErrors = true;

if (missingJSDocExports.length > 0) {
console.log(`❌ ${pkg.name} has missing JSDoc:`);
missingJSDocExports.forEach((exportInfo) => {
const relativePath = path.relative(
projectConfig.root,
exportInfo.filePath
);
console.log(` - ${relativePath}:${exportInfo.symbolName}`);
});
} else {
console.log(`✅ ${pkg.name} has JSDoc for all exports`);
continue;
}

console.log(`✅ ${pkg.name} has JSDoc for all exports`);
}

return 0;
return hasErrors ? 1 : 0;
}
}

Expand Down Expand Up @@ -108,3 +105,31 @@ function printNoPackagesFound(
console.error(` - project root: ${root}`);
console.error(` - package manager: ${packageManager}`);
}



function printValidationErrors(packageName: string, result: PackageValidationResult): void {
console.log(`❌ ${packageName} has missing JSDoc:`);

result.issues.forEach((issue) => {
issue.errors.forEach((error) => {
const message = formatErrorMessage(error);
console.log(` - ${issue.relativePath}:${issue.exportDeclaration.symbolName} - ${message}`);
});
});
}

function formatErrorMessage(error: ValidationError): string {
switch (error.type) {
case "missing_public":
return "missing @public";
case "missing_param":
return `missing @param for '${error.target}'`;
case "unused_param":
return `unused @param '${error.target}'`;
case "missing_returns":
return "missing @returns";
case "invalid_returns":
return error.message ?? "invalid @returns";
}
}
49 changes: 49 additions & 0 deletions packages/cli/src/commands/check/validate/validate-exports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { isEmpty } from "es-toolkit/compat";
import path from "path";
import { ExportDeclaration } from "../../../core/types/parser.types.js";
import { validate } from "./validate-jsdoc.js";
import { validatePublic } from "./validate-public.js";
import { ValidationError } from "./validate.types.js";

export interface ValidationIssue {
exportDeclaration: ExportDeclaration;
relativePath: string;
errors: ValidationError[];
}

export interface PackageValidationResult {
issues: ValidationIssue[];
}

export function validateExports(
exportDeclarations: ExportDeclaration[],
projectRoot: string
): PackageValidationResult {
const issues = exportDeclarations
.map((target) => {
const relativePath = path.relative(projectRoot, target.filePath);
const isPublic = validatePublic(target.declaration);

if (!isPublic) {
return {
exportDeclaration: target,
relativePath,
errors: [{ type: "missing_public" as const, target: target.symbolName }],
};
}

const { errors } = validate(target.declaration);
if (isEmpty(errors)) {
return null;
}

return {
exportDeclaration: target,
relativePath,
errors,
};
})
.filter((issue) => issue != null);

return { issues };
}
69 changes: 69 additions & 0 deletions packages/cli/src/commands/check/validate/validate-jsdoc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Node } from 'ts-morph';
import { JSDocParser } from '../../../core/parser/jsdoc/jsdoc-parser.js';
import { getJSDoc } from '../../../core/parser/jsdoc/jsdoc-utils.js';
import { ParsedJSDoc } from '../../../core/types/parser.types.js';
import { FunctionValidator } from './validator/function-validator.js';
import { InterfaceValidator } from './validator/interface-validator.js';


export function validate(node: Node) {
const validator = createValidator(node);

if (validator != null) {
return validator.validate();
}

return { errors: [], isValid: true };
}

const EMPTY_PARSED_JSDOC: ParsedJSDoc = {
examples: [],
parameters: [],
throws: [],
typedef: [],
see: [],
version: [],
};

function createValidator(node: Node) {
const jsDocParser = new JSDocParser();
const jsDoc = getJSDoc(node);
const parsedJSDoc = jsDoc != null ? jsDocParser.parse(jsDoc) : EMPTY_PARSED_JSDOC;

if (Node.isFunctionDeclaration(node)) {
return new FunctionValidator(node, parsedJSDoc);
}

if (Node.isVariableDeclaration(node)) {
const initializer = node.getInitializer();

if (Node.isArrowFunction(initializer)) {
return new FunctionValidator(node, parsedJSDoc);
}

// TODO:
// if (Node.isObjectLiteralExpression(initializer)) {
// return new ObjectLiteralValidator(node, parsedJSDoc);
// }
}


if (Node.isInterfaceDeclaration(node)) {
return new InterfaceValidator(node, parsedJSDoc);
}

// if (Node.isTypeAliasDeclaration(node)) {
// return new TypeAliasValidator(node, parsedJSDoc);
// }

// if (Node.isEnumDeclaration(node)) {
// return new EnumValidator(node, parsedJSDoc);
// }


// if (Node.isClassDeclaration(node)) {
// return new ClassValidator(node, parsedJSDoc);
// }

return undefined;
}
6 changes: 6 additions & 0 deletions packages/cli/src/commands/check/validate/validate-public.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { hasJSDocTag } from "../../../core/parser/jsdoc/jsdoc-utils.js";
import { Node } from "ts-morph";

export function validatePublic(node: Node) {
return hasJSDocTag(node, "public");
}
17 changes: 17 additions & 0 deletions packages/cli/src/commands/check/validate/validate.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
type ValidationErrorType =
| "missing_public"
| "missing_param"
| "unused_param"
| "missing_returns"
| "invalid_returns";

export interface ValidationError {
type: ValidationErrorType;
target: string;
message?: string;
}

export interface ValidationResult {
errors: ValidationError[];
isValid: boolean;
}
Loading