UNPKG

docschema

Version:

Schema declaration and validation library using JsDoc comments

395 lines (332 loc) 10.6 kB
import { dequoteString, findClosingQuotePosition, isolateEndDescription, isolateFrontDescription, removeWrappingBraces, splitTypeExpression } from '../functions/utils.js' import { parse } from '../parse-check-filters/parse.js' /** * Primitive types * * @see https://developer.mozilla.org/en-US/docs/Glossary/Primitive * @see https://github.com/jsdoc/jsdoc/issues/1066 * @type {Set<Primitives>} */ const primitiveTypes = new Set([ 'string', 'number', 'bigint', 'boolean', 'undefined', 'symbol', 'null' ]) const quotes = new Set(['\'', '"', '`']) /** * @param {string} typeExpression * @returns {string} */ function cleanTypeExpression(typeExpression) { return removeWrappingBraces(typeExpression.trim()) } /** * An object of functions that can parse simple and complex * JsDoc type expressions * - these are expressions that does not contain * other expressions in them. * * It's important for the regexes to NOT have the 'm' flag * * @type {{ * simple: { * 'any' : ParserFunctionSimple, * 'boolean' : ParserFunctionSimple, * 'number' : ParserFunctionSimple, * 'string' : ParserFunctionSimple, * 'tryPrimitive' : ParserFunctionSimple, * }, * complex: { * 'array' : ParserFunctionComplex, * 'array2' : ParserFunctionComplex, * 'literal' : ParserFunctionComplex, * 'object' : ParserFunctionComplex, * 'typedef' : ParserFunctionComplex, * 'directImport' : ParserFunctionComplex, * } * }} */ export const parsers = { simple: { /* * any or * */ any: (typeExpression) => { const typeExpressionClean = cleanTypeExpression(typeExpression) if ( typeExpressionClean !== '' && typeExpressionClean !== '*' && typeExpressionClean !== 'any' ) { return false } return { typeName: 'any', typeExpression: typeExpressionClean } }, /* * true or false */ boolean: (typeExpression) => { const typeExpressionClean = cleanTypeExpression(typeExpression) if (typeExpressionClean !== 'true' && typeExpressionClean !== 'false') { return false } return { typeName: 'boolean', typeExpression: typeExpressionClean, value: (typeExpressionClean === 'true') } }, /* * Something like: 12345 */ number: (typeExpression) => { const typeExpressionClean = cleanTypeExpression(typeExpression) if (/^-?\d+\.?\d*$/u.exec(typeExpressionClean) === null) { return false } return { typeName: 'number', typeExpression: typeExpressionClean, value: parseFloat(typeExpressionClean) } }, /* * Something like: 'some-text' */ string: (typeExpression) => { const typeExpressionClean = cleanTypeExpression(typeExpression) const quote = typeExpressionClean[0] ?? '' if (!quotes.has(quote)) return false /** * Check whether the input type is a string * value (having quotes). * For example, including the quotes: * 'text' * 'I\'m cool' */ const closingQuotePosition = findClosingQuotePosition(typeExpressionClean) if (closingQuotePosition !== typeExpressionClean.length - 1) { return false } return { typeName: 'string', typeExpression: typeExpressionClean, value: typeExpressionClean.slice(1, -1) } }, /* * Something like: "number" */ tryPrimitive: (typeExpression) => { const typeExpressionClean = cleanTypeExpression(typeExpression) /** @type {Primitives} */ // @ts-expect-error const typeName = typeExpressionClean.toLowerCase() if (!primitiveTypes.has(typeName)) { return false } return { typeName: typeName, typeExpression: typeExpressionClean } } }, complex: { /* * Something like: Array.<number> */ array: (typeExpression, typeParser, currentLocation) => { const typeExpressionClean = cleanTypeExpression(typeExpression) const match = /^array\.?<(.+)>$/ui.exec(typeExpressionClean) if (match === null) return false const arrayValueTypes = match[1] ?? '*' return { typeName: 'array', typeExpression: typeExpressionClean, types: typeParser(arrayValueTypes, currentLocation) } }, /* * Something like: string[] */ array2: (typeExpression, typeParser, currentLocation) => { const typeExpressionClean = cleanTypeExpression(typeExpression) /* * The pattern matches two situations: * 1: typeName[] * 2: (typeName)[] */ const pattern = /^((?=[^(]).+(?<=[^)]))\[\]$|^\((.*)\)\[\]$/ui const match = pattern.exec(typeExpressionClean) if (match === null) return false const arrayValueTypes = match[1] ?? match[2] ?? '' return { typeName: 'array', typeExpression: typeExpressionClean, types: typeParser(arrayValueTypes, currentLocation) } }, /* * Covers arrayLiteral and objectLiteral types * * Array literal is like: [number, string] * Object literal is like: {a:number, b:string} * * Also, it supports multiline object literal, * with or without comments as descriptions */ literal: (typeExpression, typeParser, currentLocation) => { const typeExpressionClean = cleanTypeExpression(typeExpression) const openingBracket = typeExpressionClean[0] ?? '' let pattern = null let isArray = false if (openingBracket === '{') { pattern = /^\{(.*)\}$/usi isArray = false } else if (openingBracket === '[') { pattern = /^\[(.*)\]$/usi isArray = true } else { return false } const match = pattern.exec(typeExpressionClean) if (match === null) return false const literalBody = (match?.[1] ?? '').trim() /* * When separating by comma, the first expression will * likely be fine. But it would be expected to have * single-line descriptions after the type definition. * In this case, the description for a given expression * will be placed in front of the next expression, because * it follows the comma. This means that the description * needs to be separated. */ const pairExpressions = splitTypeExpression(literalBody, new Set([','])) /** @type {ObjectLiteralPair[]} */ const pairs = [] let index = 0 for (const pairExpression of pairExpressions) { const frontDescriptionTuple = isolateFrontDescription(pairExpression) const pairsLength = pairs.length const pairsLastIndex = pairsLength - 1 if (pairsLength > 0) { const { description, filters } = parse( frontDescriptionTuple[0], // @ts-expect-error pairs[pairsLastIndex].valueTypes, currentLocation ) // @ts-expect-error pairs[pairsLastIndex].description = description // @ts-expect-error pairs[pairsLastIndex].filters = filters } /* * Only comment found => it's the comment of * the last pair */ if (frontDescriptionTuple[1] === '') { continue } let type = '' let key = '' if (isArray) { type = frontDescriptionTuple[1] ?? '' key = index.toString() index += 1 } else { const nameTypePair = splitTypeExpression( frontDescriptionTuple[1], new Set([':']) ) const name = nameTypePair[0] ?? '' type = nameTypePair[1] ?? '' key = name.replaceAll(' ', '') } const endDescriptionTuple = isolateEndDescription(type) const types = (endDescriptionTuple[0]).replace(/\s/ug, '') let valueTypes = typeParser(types, currentLocation) if (!isArray) { /* * Deal with optional parameters like {key?:string} * by removing the ? and adding undefined as one * more possible type */ if (key.endsWith('?')) { key = key.substring(0, key.length - 1) valueTypes = valueTypes.concat( typeParser('undefined', currentLocation) ) } } const { description, filters } = parse( endDescriptionTuple[1], valueTypes, currentLocation ) /** @type {ObjectLiteralPair} */ const pair = { key, valueTypes, description, filters } pairs.push(pair) } return { typeName: (isArray) ? 'arrayLiteral' : 'objectLiteral', typeExpression: typeExpressionClean, pairs: pairs } }, /* * Something like: Object.<string, string> */ object: (typeExpression, typeParser, currentLocation) => { const typeExpressionClean = cleanTypeExpression(typeExpression) const match = /^(?:object|record)\.?<(.+)>$/ui.exec(typeExpressionClean) if (match === null) return false const pairExpression = (match?.[1] ?? '').trim() const pair = splitTypeExpression(pairExpression, new Set([','])) return { typeName: 'object', typeExpression: typeExpressionClean, typePairs: [{ keyTypes: typeParser(pair[0] ?? '*', currentLocation), valueTypes: typeParser(pair[1] ?? '*', currentLocation) }] } }, /* * Something like: MyTypeName */ typedef: (typeExpression) => { const typeExpressionClean = cleanTypeExpression(typeExpression) const match = /^\w+$/u.exec(typeExpressionClean) if (match === null) return false return { typeName: 'typedef', typeExpression: typeExpressionClean } }, /* * Something like: import('./file').MyTypeName */ directImport: (typeExpression) => { const typeExpressionClean = cleanTypeExpression(typeExpression) const match = /(?:^|\n|;)import\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*\)\s*.\s*(\w+)/ugm.exec(typeExpressionClean) if (match === null) return false return { typeName: 'directImport', typeExpression: typeExpressionClean, directImport: { file: dequoteString(match[1]) ?? '', typeName: match[2] ?? '', } } } } }