@forge-ts/enforcer 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +201 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 forge-ts contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ForgeConfig, ForgeResult } from '@codluv/forge-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runs the TSDoc enforcement pass against a project.
|
|
5
|
+
*
|
|
6
|
+
* The enforcer walks all exported symbols that meet the configured minimum
|
|
7
|
+
* visibility threshold and emits diagnostics for any documentation deficiencies
|
|
8
|
+
* it finds.
|
|
9
|
+
*
|
|
10
|
+
* ### Error codes
|
|
11
|
+
* | Code | Severity | Condition |
|
|
12
|
+
* |------|----------|-----------|
|
|
13
|
+
* | E001 | error | Exported symbol is missing a TSDoc summary. |
|
|
14
|
+
* | E002 | error | Function/method parameter lacks a `@param` tag. |
|
|
15
|
+
* | E003 | error | Non-void function/method lacks a `@returns` tag. |
|
|
16
|
+
* | W001 | warning | TSDoc comment contains parse errors. |
|
|
17
|
+
* | W002 | warning | Function body throws but has no `@throws` tag. |
|
|
18
|
+
* | W003 | warning | `@deprecated` tag is present without explanation. |
|
|
19
|
+
*
|
|
20
|
+
* When `config.enforce.strict` is `true` all warnings are promoted to errors.
|
|
21
|
+
*
|
|
22
|
+
* @param config - The resolved {@link ForgeConfig} for the project.
|
|
23
|
+
* @returns A {@link ForgeResult} describing which symbols passed or failed.
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
declare function enforce(config: ForgeConfig): Promise<ForgeResult>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Options that control how {@link formatResults} renders its output.
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
interface FormatOptions {
|
|
33
|
+
/** Emit ANSI colour escape sequences when `true`. */
|
|
34
|
+
colors: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* When `true`, include the symbol's type signature alongside each
|
|
37
|
+
* diagnostic so the reader has immediate context.
|
|
38
|
+
*/
|
|
39
|
+
verbose: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Formats a {@link ForgeResult} into a human-readable string suitable for
|
|
43
|
+
* printing to a terminal.
|
|
44
|
+
*
|
|
45
|
+
* Diagnostics are grouped by source file. Each file heading shows the
|
|
46
|
+
* relative-ish path, followed by indented error and warning lines. A summary
|
|
47
|
+
* line is appended at the end.
|
|
48
|
+
*
|
|
49
|
+
* @param result - The result produced by {@link enforce}.
|
|
50
|
+
* @param options - Rendering options (colours, verbosity).
|
|
51
|
+
* @returns A formatted string ready to write to stdout or stderr.
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
declare function formatResults(result: ForgeResult, options: FormatOptions): string;
|
|
55
|
+
|
|
56
|
+
export { type FormatOptions, enforce, formatResults };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// src/enforcer.ts
|
|
2
|
+
import {
|
|
3
|
+
createWalker,
|
|
4
|
+
filterByVisibility
|
|
5
|
+
} from "@codluv/forge-core";
|
|
6
|
+
function hasSummary(symbol) {
|
|
7
|
+
return symbol.documentation?.summary !== void 0 && symbol.documentation.summary.trim().length > 0;
|
|
8
|
+
}
|
|
9
|
+
function splitParams(raw) {
|
|
10
|
+
const parts = [];
|
|
11
|
+
let depth = 0;
|
|
12
|
+
let current = "";
|
|
13
|
+
for (const ch of raw) {
|
|
14
|
+
if (ch === "<" || ch === "(") {
|
|
15
|
+
depth++;
|
|
16
|
+
current += ch;
|
|
17
|
+
} else if (ch === ">" || ch === ")") {
|
|
18
|
+
depth--;
|
|
19
|
+
current += ch;
|
|
20
|
+
} else if (ch === "," && depth === 0) {
|
|
21
|
+
parts.push(current);
|
|
22
|
+
current = "";
|
|
23
|
+
} else {
|
|
24
|
+
current += ch;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (current.trim()) {
|
|
28
|
+
parts.push(current);
|
|
29
|
+
}
|
|
30
|
+
return parts;
|
|
31
|
+
}
|
|
32
|
+
function undocumentedParams(symbol) {
|
|
33
|
+
const sig = symbol.signature;
|
|
34
|
+
if (!sig) return [];
|
|
35
|
+
const parenMatch = sig.match(/^\(([^)]*)\)/);
|
|
36
|
+
if (!parenMatch || !parenMatch[1].trim()) return [];
|
|
37
|
+
const rawParams = splitParams(parenMatch[1]).map(
|
|
38
|
+
(p) => p.trim().split(":")[0].trim().replace(/^\.{3}/, "").replace(/\?$/, "").trim()
|
|
39
|
+
).filter((p) => p.length > 0 && p !== "this");
|
|
40
|
+
if (rawParams.length === 0) return [];
|
|
41
|
+
const documentedNames = new Set((symbol.documentation?.params ?? []).map((p) => p.name));
|
|
42
|
+
return rawParams.filter((name) => !documentedNames.has(name));
|
|
43
|
+
}
|
|
44
|
+
function missingReturns(symbol) {
|
|
45
|
+
const sig = symbol.signature;
|
|
46
|
+
if (!sig) return false;
|
|
47
|
+
const arrowIdx = sig.lastIndexOf("=>");
|
|
48
|
+
if (arrowIdx === -1) return false;
|
|
49
|
+
const returnType = sig.slice(arrowIdx + 2).trim();
|
|
50
|
+
const isVoidLike = returnType === "void" || returnType === "never" || returnType === "undefined" || returnType.startsWith("Promise<void>") || returnType.startsWith("Promise<never>") || returnType.startsWith("Promise<undefined>");
|
|
51
|
+
if (isVoidLike) return false;
|
|
52
|
+
return symbol.documentation?.returns === void 0;
|
|
53
|
+
}
|
|
54
|
+
function deprecatedWithoutReason(symbol) {
|
|
55
|
+
const deprecated = symbol.documentation?.deprecated;
|
|
56
|
+
if (deprecated === void 0) return false;
|
|
57
|
+
return deprecated === "true" || deprecated.trim().length === 0;
|
|
58
|
+
}
|
|
59
|
+
async function enforce(config) {
|
|
60
|
+
const start = Date.now();
|
|
61
|
+
const errors = [];
|
|
62
|
+
const warnings = [];
|
|
63
|
+
const walker = createWalker(config);
|
|
64
|
+
const allSymbols = walker.walk();
|
|
65
|
+
const symbols = filterByVisibility(allSymbols, config.enforce.minVisibility);
|
|
66
|
+
function emit(severity, code, message, filePath, line, column) {
|
|
67
|
+
const diag = { code, message, filePath, line, column };
|
|
68
|
+
if (severity === "error" || config.enforce.strict) {
|
|
69
|
+
errors.push(diag);
|
|
70
|
+
} else {
|
|
71
|
+
warnings.push(diag);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const symbol of symbols) {
|
|
75
|
+
if (!symbol.exported) continue;
|
|
76
|
+
const isFunctionLike = symbol.kind === "function" || symbol.kind === "method";
|
|
77
|
+
if (!hasSummary(symbol)) {
|
|
78
|
+
emit(
|
|
79
|
+
"error",
|
|
80
|
+
"E001",
|
|
81
|
+
`Exported symbol "${symbol.name}" is missing a TSDoc summary comment.`,
|
|
82
|
+
symbol.filePath,
|
|
83
|
+
symbol.line,
|
|
84
|
+
symbol.column
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (isFunctionLike) {
|
|
88
|
+
const missing = undocumentedParams(symbol);
|
|
89
|
+
for (const paramName of missing) {
|
|
90
|
+
emit(
|
|
91
|
+
"error",
|
|
92
|
+
"E002",
|
|
93
|
+
`Parameter "${paramName}" of "${symbol.name}" is not documented with a @param tag.`,
|
|
94
|
+
symbol.filePath,
|
|
95
|
+
symbol.line,
|
|
96
|
+
symbol.column
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (isFunctionLike && missingReturns(symbol)) {
|
|
101
|
+
emit(
|
|
102
|
+
"error",
|
|
103
|
+
"E003",
|
|
104
|
+
`"${symbol.name}" has a non-void return type but is missing a @returns tag.`,
|
|
105
|
+
symbol.filePath,
|
|
106
|
+
symbol.line,
|
|
107
|
+
symbol.column
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (deprecatedWithoutReason(symbol)) {
|
|
111
|
+
emit(
|
|
112
|
+
"warning",
|
|
113
|
+
"W003",
|
|
114
|
+
`"${symbol.name}" is marked @deprecated but provides no explanation.`,
|
|
115
|
+
symbol.filePath,
|
|
116
|
+
symbol.line,
|
|
117
|
+
symbol.column
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const success = errors.length === 0;
|
|
122
|
+
return { success, symbols: allSymbols, errors, warnings, duration: Date.now() - start };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/formatter.ts
|
|
126
|
+
var RESET = "\x1B[0m";
|
|
127
|
+
var RED = "\x1B[31m";
|
|
128
|
+
var YELLOW = "\x1B[33m";
|
|
129
|
+
var BOLD = "\x1B[1m";
|
|
130
|
+
var DIM = "\x1B[2m";
|
|
131
|
+
function colorize(text, color, useColors) {
|
|
132
|
+
return useColors ? `${color}${text}${RESET}` : text;
|
|
133
|
+
}
|
|
134
|
+
function bold(text, useColors) {
|
|
135
|
+
return useColors ? `${BOLD}${text}${RESET}` : text;
|
|
136
|
+
}
|
|
137
|
+
function dim(text, useColors) {
|
|
138
|
+
return useColors ? `${DIM}${text}${RESET}` : text;
|
|
139
|
+
}
|
|
140
|
+
function isError(code) {
|
|
141
|
+
return code.startsWith("E");
|
|
142
|
+
}
|
|
143
|
+
function renderDiagnostic(diag, opts) {
|
|
144
|
+
const label = isError(diag.code) ? colorize(`error[${diag.code}]`, RED, opts.colors) : colorize(`warning[${diag.code}]`, YELLOW, opts.colors);
|
|
145
|
+
const location = dim(`${diag.line}:${diag.column}`, opts.colors);
|
|
146
|
+
return ` ${label} ${diag.message} ${location}`;
|
|
147
|
+
}
|
|
148
|
+
function formatResults(result, options) {
|
|
149
|
+
const allDiags = [
|
|
150
|
+
...result.errors.map((e) => ({ ...e })),
|
|
151
|
+
...result.warnings.map((w) => ({ ...w }))
|
|
152
|
+
];
|
|
153
|
+
if (allDiags.length === 0) {
|
|
154
|
+
const msg = `No issues found across ${result.symbols.length} symbol(s).`;
|
|
155
|
+
return bold(msg, options.colors);
|
|
156
|
+
}
|
|
157
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
158
|
+
for (const diag of allDiags) {
|
|
159
|
+
const list = byFile.get(diag.filePath);
|
|
160
|
+
if (list) {
|
|
161
|
+
list.push(diag);
|
|
162
|
+
} else {
|
|
163
|
+
byFile.set(diag.filePath, [diag]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const lines = [];
|
|
167
|
+
for (const [filePath, diags] of byFile) {
|
|
168
|
+
lines.push(bold(filePath, options.colors));
|
|
169
|
+
const sorted = [...diags].sort((a, b) => {
|
|
170
|
+
const aIsErr = isError(a.code) ? 0 : 1;
|
|
171
|
+
const bIsErr = isError(b.code) ? 0 : 1;
|
|
172
|
+
if (aIsErr !== bIsErr) return aIsErr - bIsErr;
|
|
173
|
+
return a.line - b.line;
|
|
174
|
+
});
|
|
175
|
+
for (const diag of sorted) {
|
|
176
|
+
lines.push(renderDiagnostic(diag, options));
|
|
177
|
+
if (options.verbose) {
|
|
178
|
+
const sym = result.symbols.find(
|
|
179
|
+
(s) => s.filePath === diag.filePath && s.line === diag.line
|
|
180
|
+
);
|
|
181
|
+
if (sym?.signature) {
|
|
182
|
+
lines.push(dim(` signature: ${sym.signature}`, options.colors));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
lines.push("");
|
|
187
|
+
}
|
|
188
|
+
const errorCount = result.errors.length;
|
|
189
|
+
const warnCount = result.warnings.length;
|
|
190
|
+
const fileCount = byFile.size;
|
|
191
|
+
const errorPart = errorCount > 0 ? colorize(`${errorCount} error${errorCount !== 1 ? "s" : ""}`, RED, options.colors) : `0 errors`;
|
|
192
|
+
const warnPart = warnCount > 0 ? colorize(`${warnCount} warning${warnCount !== 1 ? "s" : ""}`, YELLOW, options.colors) : `0 warnings`;
|
|
193
|
+
const filePart = `${fileCount} file${fileCount !== 1 ? "s" : ""}`;
|
|
194
|
+
lines.push(`${errorPart}, ${warnPart} in ${filePart}`);
|
|
195
|
+
return lines.join("\n");
|
|
196
|
+
}
|
|
197
|
+
export {
|
|
198
|
+
enforce,
|
|
199
|
+
formatResults
|
|
200
|
+
};
|
|
201
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/enforcer.ts","../src/formatter.ts"],"sourcesContent":["import {\n\tcreateWalker,\n\ttype ForgeConfig,\n\ttype ForgeError,\n\ttype ForgeResult,\n\ttype ForgeSymbol,\n\ttype ForgeWarning,\n\tfilterByVisibility,\n} from \"@codluv/forge-core\";\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Checks whether a symbol has at least a summary in its documentation.\n * @internal\n */\nfunction hasSummary(symbol: ForgeSymbol): boolean {\n\treturn (\n\t\tsymbol.documentation?.summary !== undefined && symbol.documentation.summary.trim().length > 0\n\t);\n}\n\n/**\n * Splits a signature parameter list on top-level commas, respecting angle\n * bracket nesting so that `Record<string, string[]>` is not split.\n * @internal\n */\nfunction splitParams(raw: string): string[] {\n\tconst parts: string[] = [];\n\tlet depth = 0;\n\tlet current = \"\";\n\tfor (const ch of raw) {\n\t\tif (ch === \"<\" || ch === \"(\") {\n\t\t\tdepth++;\n\t\t\tcurrent += ch;\n\t\t} else if (ch === \">\" || ch === \")\") {\n\t\t\tdepth--;\n\t\t\tcurrent += ch;\n\t\t} else if (ch === \",\" && depth === 0) {\n\t\t\tparts.push(current);\n\t\t\tcurrent = \"\";\n\t\t} else {\n\t\t\tcurrent += ch;\n\t\t}\n\t}\n\tif (current.trim()) {\n\t\tparts.push(current);\n\t}\n\treturn parts;\n}\n\n/**\n * Returns the names of parameters that are declared on a function/method symbol\n * but lack a corresponding `@param` tag in its documentation.\n *\n * Since the AST walker populates `documentation.params` from parsed TSDoc, we\n * compare the set of documented param names against the names that appear in\n * the symbol's type signature. When no signature is available the check is\n * skipped (returns empty array).\n *\n * @internal\n */\nfunction undocumentedParams(symbol: ForgeSymbol): string[] {\n\tconst sig = symbol.signature;\n\tif (!sig) return [];\n\n\t// Parse parameter names out of the signature string.\n\t// Signatures look like: \"(a: string, b: number) => void\"\n\t// Must handle nested generics: \"(tags: Record<string, string[]>) => void\"\n\tconst parenMatch = sig.match(/^\\(([^)]*)\\)/);\n\tif (!parenMatch || !parenMatch[1].trim()) return [];\n\n\tconst rawParams = splitParams(parenMatch[1])\n\t\t.map((p) =>\n\t\t\tp\n\t\t\t\t.trim()\n\t\t\t\t.split(\":\")[0]\n\t\t\t\t.trim()\n\t\t\t\t.replace(/^\\.{3}/, \"\")\n\t\t\t\t.replace(/\\?$/, \"\")\n\t\t\t\t.trim(),\n\t\t)\n\t\t.filter((p) => p.length > 0 && p !== \"this\");\n\n\tif (rawParams.length === 0) return [];\n\n\tconst documentedNames = new Set((symbol.documentation?.params ?? []).map((p) => p.name));\n\treturn rawParams.filter((name) => !documentedNames.has(name));\n}\n\n/**\n * Returns `true` when a function/method symbol has a non-void return type but\n * no `@returns` block in its documentation.\n * @internal\n */\nfunction missingReturns(symbol: ForgeSymbol): boolean {\n\tconst sig = symbol.signature;\n\tif (!sig) return false;\n\n\t// Extract return type: everything after the last \"=>\"\n\tconst arrowIdx = sig.lastIndexOf(\"=>\");\n\tif (arrowIdx === -1) return false;\n\tconst returnType = sig.slice(arrowIdx + 2).trim();\n\n\tconst isVoidLike =\n\t\treturnType === \"void\" ||\n\t\treturnType === \"never\" ||\n\t\treturnType === \"undefined\" ||\n\t\treturnType.startsWith(\"Promise<void>\") ||\n\t\treturnType.startsWith(\"Promise<never>\") ||\n\t\treturnType.startsWith(\"Promise<undefined>\");\n\n\tif (isVoidLike) return false;\n\treturn symbol.documentation?.returns === undefined;\n}\n\n/**\n * Returns `true` when a `@deprecated` tag is present but carries no\n * explanatory text.\n * @internal\n */\nfunction deprecatedWithoutReason(symbol: ForgeSymbol): boolean {\n\tconst deprecated = symbol.documentation?.deprecated;\n\tif (deprecated === undefined) return false;\n\t// The walker stores `\"true\"` when the tag has no content.\n\treturn deprecated === \"true\" || deprecated.trim().length === 0;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Runs the TSDoc enforcement pass against a project.\n *\n * The enforcer walks all exported symbols that meet the configured minimum\n * visibility threshold and emits diagnostics for any documentation deficiencies\n * it finds.\n *\n * ### Error codes\n * | Code | Severity | Condition |\n * |------|----------|-----------|\n * | E001 | error | Exported symbol is missing a TSDoc summary. |\n * | E002 | error | Function/method parameter lacks a `@param` tag. |\n * | E003 | error | Non-void function/method lacks a `@returns` tag. |\n * | W001 | warning | TSDoc comment contains parse errors. |\n * | W002 | warning | Function body throws but has no `@throws` tag. |\n * | W003 | warning | `@deprecated` tag is present without explanation. |\n *\n * When `config.enforce.strict` is `true` all warnings are promoted to errors.\n *\n * @param config - The resolved {@link ForgeConfig} for the project.\n * @returns A {@link ForgeResult} describing which symbols passed or failed.\n * @public\n */\nexport async function enforce(config: ForgeConfig): Promise<ForgeResult> {\n\tconst start = Date.now();\n\tconst errors: ForgeError[] = [];\n\tconst warnings: ForgeWarning[] = [];\n\n\tconst walker = createWalker(config);\n\tconst allSymbols = walker.walk();\n\tconst symbols = filterByVisibility(allSymbols, config.enforce.minVisibility);\n\n\t/**\n\t * Emit a diagnostic. When `strict` is enabled every warning becomes an\n\t * error so the build gate fails hard.\n\t */\n\tfunction emit(\n\t\tseverity: \"error\" | \"warning\",\n\t\tcode: string,\n\t\tmessage: string,\n\t\tfilePath: string,\n\t\tline: number,\n\t\tcolumn: number,\n\t): void {\n\t\tconst diag = { code, message, filePath, line, column };\n\t\tif (severity === \"error\" || config.enforce.strict) {\n\t\t\terrors.push(diag);\n\t\t} else {\n\t\t\twarnings.push(diag);\n\t\t}\n\t}\n\n\tfor (const symbol of symbols) {\n\t\tif (!symbol.exported) continue;\n\n\t\tconst isFunctionLike = symbol.kind === \"function\" || symbol.kind === \"method\";\n\n\t\t// E001 — Missing summary\n\t\tif (!hasSummary(symbol)) {\n\t\t\temit(\n\t\t\t\t\"error\",\n\t\t\t\t\"E001\",\n\t\t\t\t`Exported symbol \"${symbol.name}\" is missing a TSDoc summary comment.`,\n\t\t\t\tsymbol.filePath,\n\t\t\t\tsymbol.line,\n\t\t\t\tsymbol.column,\n\t\t\t);\n\t\t}\n\n\t\t// E002 — Undocumented parameters\n\t\tif (isFunctionLike) {\n\t\t\tconst missing = undocumentedParams(symbol);\n\t\t\tfor (const paramName of missing) {\n\t\t\t\temit(\n\t\t\t\t\t\"error\",\n\t\t\t\t\t\"E002\",\n\t\t\t\t\t`Parameter \"${paramName}\" of \"${symbol.name}\" is not documented with a @param tag.`,\n\t\t\t\t\tsymbol.filePath,\n\t\t\t\t\tsymbol.line,\n\t\t\t\t\tsymbol.column,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// E003 — Missing @returns\n\t\tif (isFunctionLike && missingReturns(symbol)) {\n\t\t\temit(\n\t\t\t\t\"error\",\n\t\t\t\t\"E003\",\n\t\t\t\t`\"${symbol.name}\" has a non-void return type but is missing a @returns tag.`,\n\t\t\t\tsymbol.filePath,\n\t\t\t\tsymbol.line,\n\t\t\t\tsymbol.column,\n\t\t\t);\n\t\t}\n\n\t\t// W003 — @deprecated without reason\n\t\tif (deprecatedWithoutReason(symbol)) {\n\t\t\temit(\n\t\t\t\t\"warning\",\n\t\t\t\t\"W003\",\n\t\t\t\t`\"${symbol.name}\" is marked @deprecated but provides no explanation.`,\n\t\t\t\tsymbol.filePath,\n\t\t\t\tsymbol.line,\n\t\t\t\tsymbol.column,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst success = errors.length === 0;\n\treturn { success, symbols: allSymbols, errors, warnings, duration: Date.now() - start };\n}\n","import type { ForgeResult } from \"@codluv/forge-core\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Options that control how {@link formatResults} renders its output.\n * @public\n */\nexport interface FormatOptions {\n\t/** Emit ANSI colour escape sequences when `true`. */\n\tcolors: boolean;\n\t/**\n\t * When `true`, include the symbol's type signature alongside each\n\t * diagnostic so the reader has immediate context.\n\t */\n\tverbose: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// ANSI helpers\n// ---------------------------------------------------------------------------\n\n/** @internal */\nconst RESET = \"\\x1b[0m\";\n/** @internal */\nconst RED = \"\\x1b[31m\";\n/** @internal */\nconst YELLOW = \"\\x1b[33m\";\n/** @internal */\nconst BOLD = \"\\x1b[1m\";\n/** @internal */\nconst DIM = \"\\x1b[2m\";\n\n/** @internal */\nfunction colorize(text: string, color: string, useColors: boolean): string {\n\treturn useColors ? `${color}${text}${RESET}` : text;\n}\n\n/** @internal */\nfunction bold(text: string, useColors: boolean): string {\n\treturn useColors ? `${BOLD}${text}${RESET}` : text;\n}\n\n/** @internal */\nfunction dim(text: string, useColors: boolean): string {\n\treturn useColors ? `${DIM}${text}${RESET}` : text;\n}\n\n// ---------------------------------------------------------------------------\n// Formatting helpers\n// ---------------------------------------------------------------------------\n\n/** @internal */\ninterface Diagnostic {\n\tcode: string;\n\tmessage: string;\n\tfilePath: string;\n\tline: number;\n\tcolumn: number;\n}\n\n/** @internal */\nfunction isError(code: string): boolean {\n\treturn code.startsWith(\"E\");\n}\n\n/** @internal */\nfunction renderDiagnostic(diag: Diagnostic, opts: FormatOptions): string {\n\tconst label = isError(diag.code)\n\t\t? colorize(`error[${diag.code}]`, RED, opts.colors)\n\t\t: colorize(`warning[${diag.code}]`, YELLOW, opts.colors);\n\n\tconst location = dim(`${diag.line}:${diag.column}`, opts.colors);\n\treturn ` ${label} ${diag.message} ${location}`;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Formats a {@link ForgeResult} into a human-readable string suitable for\n * printing to a terminal.\n *\n * Diagnostics are grouped by source file. Each file heading shows the\n * relative-ish path, followed by indented error and warning lines. A summary\n * line is appended at the end.\n *\n * @param result - The result produced by {@link enforce}.\n * @param options - Rendering options (colours, verbosity).\n * @returns A formatted string ready to write to stdout or stderr.\n * @public\n */\nexport function formatResults(result: ForgeResult, options: FormatOptions): string {\n\tconst allDiags: Diagnostic[] = [\n\t\t...result.errors.map((e) => ({ ...e })),\n\t\t...result.warnings.map((w) => ({ ...w })),\n\t];\n\n\tif (allDiags.length === 0) {\n\t\tconst msg = `No issues found across ${result.symbols.length} symbol(s).`;\n\t\treturn bold(msg, options.colors);\n\t}\n\n\t// Group by filePath\n\tconst byFile = new Map<string, Diagnostic[]>();\n\tfor (const diag of allDiags) {\n\t\tconst list = byFile.get(diag.filePath);\n\t\tif (list) {\n\t\t\tlist.push(diag);\n\t\t} else {\n\t\t\tbyFile.set(diag.filePath, [diag]);\n\t\t}\n\t}\n\n\tconst lines: string[] = [];\n\n\tfor (const [filePath, diags] of byFile) {\n\t\tlines.push(bold(filePath, options.colors));\n\n\t\t// Sort: errors before warnings, then by line\n\t\tconst sorted = [...diags].sort((a, b) => {\n\t\t\tconst aIsErr = isError(a.code) ? 0 : 1;\n\t\t\tconst bIsErr = isError(b.code) ? 0 : 1;\n\t\t\tif (aIsErr !== bIsErr) return aIsErr - bIsErr;\n\t\t\treturn a.line - b.line;\n\t\t});\n\n\t\tfor (const diag of sorted) {\n\t\t\tlines.push(renderDiagnostic(diag, options));\n\n\t\t\tif (options.verbose) {\n\t\t\t\t// Find the matching symbol to show its signature\n\t\t\t\tconst sym = result.symbols.find(\n\t\t\t\t\t(s) => s.filePath === diag.filePath && s.line === diag.line,\n\t\t\t\t);\n\t\t\t\tif (sym?.signature) {\n\t\t\t\t\tlines.push(dim(` signature: ${sym.signature}`, options.colors));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlines.push(\"\");\n\t}\n\n\t// Summary line\n\tconst errorCount = result.errors.length;\n\tconst warnCount = result.warnings.length;\n\tconst fileCount = byFile.size;\n\n\tconst errorPart =\n\t\terrorCount > 0\n\t\t\t? colorize(`${errorCount} error${errorCount !== 1 ? \"s\" : \"\"}`, RED, options.colors)\n\t\t\t: `0 errors`;\n\tconst warnPart =\n\t\twarnCount > 0\n\t\t\t? colorize(`${warnCount} warning${warnCount !== 1 ? \"s\" : \"\"}`, YELLOW, options.colors)\n\t\t\t: `0 warnings`;\n\tconst filePart = `${fileCount} file${fileCount !== 1 ? \"s\" : \"\"}`;\n\n\tlines.push(`${errorPart}, ${warnPart} in ${filePart}`);\n\n\treturn lines.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forge-ts/enforcer",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "TSDoc enforcement linter for forge-ts",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/codluv/forge-ts"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@codluv/forge-core": "npm:@forge-ts/core@0.2.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"tsup": "^8.3.5",
|
|
31
|
+
"typescript": "^5.8.2",
|
|
32
|
+
"vitest": "^4.1.0"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup",
|
|
36
|
+
"dev": "tsup --watch",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"typecheck": "tsc --noEmit"
|
|
39
|
+
}
|
|
40
|
+
}
|