1/**
2 * Validates parsed documentation against the configured requirements.
3 *
4 * @since 0.6.0
5 */
6import { codeFrameColumns } from "@babel/code-frame"
7import * as Array from "effect/Array"
8import * as Effect from "effect/Effect"
9import * as Configuration from "./Configuration.ts"
10import type * as Domain from "./Domain.ts"
11import * as Parser from "./Parser.ts"
12
13const makeError = (
14 source: Parser.SourceShape,
15 position: Domain.Position,
16 message: (filePath: string, frame: string) => string
17) => {
18 const location = { start: position }
19 const frame = codeFrameColumns(source.sourceFile.getFullText(), location)
20 return [message(source.sourceFile.getFilePath(), frame)]
21}
22
23type Entry = {
24 readonly doc: Domain.Doc
25 readonly position: Domain.Position
26}
27
28function checkEntry(model: Entry, options: {
29 readonly enforceVersion: boolean
30}) {
31 return Effect.gen(function*() {
32 const source = yield* Parser.Source
33 const config = yield* Configuration.Configuration
34
35 let errors: Array<string> = []
36
37 // description
38 if (config.enforceDescriptions) {
39 if (model.doc.description === undefined) {
40 errors = errors.concat(makeError(
41 source,
42 model.position,
43 (filePath, frame) => `Missing description in file ${filePath}:\n\n${frame}`
44 ))
45 }
46 }
47
48 // @example tags
49 if (config.enforceExamples) {
50 if (model.doc.examples.length === 0) {
51 errors = errors.concat(makeError(
52 source,
53 model.position,
54 (filePath, frame) => `Missing examples in file ${filePath}:\n\n${frame}`
55 ))
56 }
57 }
58
59 // @since tags
60 if (config.enforceVersion && options.enforceVersion !== false) {
61 const since = model.doc.since
62 if (since.length === 0) {
63 errors = errors.concat(makeError(
64 source,
65 model.position,
66 (filePath, frame) => `Missing \`@since\` tag in file ${filePath}:\n\n${frame}`
67 ))
68 }
69 }
70
71 return errors
72 })
73}
74
75function checkEntries(models: ReadonlyArray<Entry>, options: {
76 readonly enforceVersion: boolean
77}) {
78 return Effect.forEach(models, (model) => checkEntry(model, options)).pipe(Effect.map(Array.flatten))
79}
80
81function checkFunction(model: Domain.Function) {
82 return checkEntry(model, {
83 enforceVersion: true
84 })
85}
86
87/**
88 * Validates documentation for function declarations.
89 *
90 * @category validation
91 * @since 0.6.0
92 */
93export function checkFunctions(models: ReadonlyArray<Domain.Function>) {
94 return Effect.forEach(models, checkFunction).pipe(Effect.map(Array.flatten))
95}
96
97function checkClass(model: Domain.Class) {
98 return Effect.gen(function*() {
99 const docErrors = yield* checkEntry(model, {
100 enforceVersion: true
101 })
102 const staticMethodsErrors = yield* checkEntries(model.staticMethods, {
103 enforceVersion: false
104 })
105 const methodsErrors = yield* checkEntries(model.methods, {
106 enforceVersion: false
107 })
108 const propertiesErrors = yield* checkEntries(model.properties, {
109 enforceVersion: false
110 })
111 return Array.flatten([docErrors, staticMethodsErrors, methodsErrors, propertiesErrors])
112 })
113}
114
115/**
116 * Validates documentation for class declarations and their documented members.
117 *
118 * @category validation
119 * @since 0.6.0
120 */
121export function checkClasses(models: ReadonlyArray<Domain.Class>) {
122 return Effect.forEach(models, checkClass).pipe(Effect.map(Array.flatten))
123}
124
125function checkConstant(model: Domain.Constant) {
126 return checkEntry(model, {
127 enforceVersion: true
128 })
129}
130
131/**
132 * Validates documentation for constant declarations.
133 *
134 * @category validation
135 * @since 0.6.0
136 */
137export function checkConstants(models: ReadonlyArray<Domain.Constant>) {
138 return Effect.forEach(models, checkConstant).pipe(Effect.map(Array.flatten))
139}
140
141function checkInterface(model: Domain.Interface) {
142 return checkEntry(model, {
143 enforceVersion: true
144 })
145}
146
147/**
148 * Validates documentation for interface declarations.
149 *
150 * @category validation
151 * @since 0.6.0
152 */
153export function checkInterfaces(models: ReadonlyArray<Domain.Interface>) {
154 return Effect.forEach(models, checkInterface).pipe(Effect.map(Array.flatten))
155}
156
157function checkTypeAlias(model: Domain.TypeAlias) {
158 return checkEntry(model, {
159 enforceVersion: true
160 })
161}
162
163/**
164 * Validates documentation for type alias declarations.
165 *
166 * @category validation
167 * @since 0.6.0
168 */
169export function checkTypeAliases(models: ReadonlyArray<Domain.TypeAlias>) {
170 return Effect.forEach(models, checkTypeAlias).pipe(Effect.map(Array.flatten))
171}
172
173function checkNamespace(
174 model: Domain.Namespace
175): Effect.Effect<Array<string>, never, Parser.Source | Configuration.Configuration> {
176 return Effect.gen(function*() {
177 const docErrors = yield* checkEntry(model, {
178 enforceVersion: true
179 })
180 const interfacesErrors = yield* checkInterfaces(model.interfaces)
181 const typeAliasesErrors = yield* checkTypeAliases(model.typeAliases)
182 const namespacesErrors = yield* checkNamespaces(model.namespaces)
183 return Array.flatten([docErrors, interfacesErrors, typeAliasesErrors, namespacesErrors])
184 })
185}
186
187/**
188 * Validates documentation for namespaces and their nested declarations.
189 *
190 * @category validation
191 * @since 0.6.0
192 */
193export function checkNamespaces(models: ReadonlyArray<Domain.Namespace>) {
194 return Effect.forEach(models, checkNamespace).pipe(Effect.map(Array.flatten))
195}
196
197function checkExport(model: Domain.Export) {
198 return checkEntry(model, {
199 enforceVersion: true
200 })
201}
202
203/**
204 * Validates documentation for explicit export declarations.
205 *
206 * @category validation
207 * @since 0.6.0
208 */
209export function checkExports(models: ReadonlyArray<Domain.Export>) {
210 return Effect.forEach(models, checkExport).pipe(Effect.map(Array.flatten))
211}
212
213/**
214 * Validates every documented declaration in a parsed module.
215 *
216 * @category validation
217 * @since 0.6.0
218 */
219export function checkModule(module: Domain.Module) {
220 return Effect.gen(function*() {
221 const functionsErrors = yield* checkFunctions(module.functions)
222 const classesErrors = yield* checkClasses(module.classes)
223 const constantsErrors = yield* checkConstants(module.constants)
224 const interfacesErrors = yield* checkInterfaces(module.interfaces)
225 const typeAliasesErrors = yield* checkTypeAliases(module.typeAliases)
226 const namespacesErrors = yield* checkNamespaces(module.namespaces)
227 const exportsErrors = yield* checkExports(module.exports)
228 return Array.flatten([
229 functionsErrors,
230 classesErrors,
231 constantsErrors,
232 interfacesErrors,
233 typeAliasesErrors,
234 namespacesErrors,
235 exportsErrors
236 ])
237 }).pipe(Effect.provideService(Parser.Source, module.source))
238}
239
240/**
241 * Validates every documented declaration in a collection of parsed modules.
242 *
243 * @category validation
244 * @since 0.6.0
245 */
246export function checkModules(modules: ReadonlyArray<Domain.Module>) {
247 return Effect.forEach(modules, checkModule).pipe(Effect.map(Array.flatten))
248}