@typedefs/parser
Version:
The Parser For JSDoc Types.
216 lines (202 loc) • 5.54 kB
JavaScript
const { Fn, fn,
nullable, nonNullable,
optional, any,
} = require('./tokens');
/**
* Splits the string into tokens.
* @param {string} s
*/
const lex = (s) => {
const res = s.split(/([!?=*(),:.<>{}|\s+])/g)
return res
.filter(a => /\S/.test(a))
.map((a) => {
switch (a) {
case 'function': return fn
case 'Function': return Fn
case '!': return nonNullable
case '?': return nullable
case '=': return optional
case '*': return any
}
return a
})
}
function parser(tokens) {
let c = 0
const peek = () => tokens[c]
const peekNext = (n = 1) => tokens[c + n]
const consume = () => tokens[c++]
const parseFunction = () => {
/** @type {_typedefsParser.FunctionType} */
const meta = { return: null, args: [] }
if (peek() != '(') throw new Error('Expecting opening (')
consume()
let foundArgs
while(peek() != ')') {
if (foundArgs && peek() == 'this')
throw new Error('this must come first in function arguments')
if (foundArgs && peek() == 'new')
throw new Error('new must come first in function arguments')
if (peek() == 'this') {
consume()
if (peek() != ':') throw new Error('Expecting :')
consume()
meta.this = parseType()
} else if (peek() == 'new') {
consume()
if (peek() != ':') throw new Error('Expecting :')
consume()
meta.new = parseType()
} else if (peek() == '.' && peekNext() == '.' && peekNext(2) == '.') {
consume()
consume()
consume()
const type = parseType()
if (peek() != ')') throw new Error('Variable args must come last')
meta.variableArgs = type
} else {
const arg = parseType()
meta.args.push(arg)
if (peek() == '=') {
arg.optional = true
consume()
}
}
foundArgs = true
if (peek() == ')') {
break
}
if (peek() == ',') consume()
else throw new Error('Expecting , between arguments')
}
consume()
if (peek() == ':') {
consume()
const ret = parseType()
if (ret.name == undefined && ret.nullable)
// special case
ret.name = ''
meta.return = ret
}
return meta
}
const parseApplication = () => {
const apps = []
while(peek() != '>') {
const application = parseType()
apps.push(application)
if (peek() == '>') {
break
}
if (peek() == ',') consume()
else throw new Error('Expecting , between applications')
}
consume()
return apps
}
const parseRecord = () => {
const props = {}
while(peek() != '}') {
const propName = peek()
consume()
props[propName] = null
if (peek() == ':') {
consume()
try {
const type = parseType()
props[propName] = type
} catch (err) {
err.message += `(when parsing ${propName} property)`
throw err
}
}
if(peek() == '}') {
consume()
break
}
if (peek() != ',') {
throw new Error(`Expecting , for record after ${propName}`)
}
consume()
}
return props
}
const parseType = (specialAllowed = true, union = []) => {
/** @type {!_typedefsParser.Type} */
let type = {}
let applicationWithDot
/** @type {string} */
let token = peek()
if ([nullable, nonNullable].includes(token)) {
// can repeat for all specials but don't
if (!specialAllowed) throw new Error(`${token} not allowed after .`)
type.nullable = token === nullable
consume()
}
token = peek()
if (token == '(') { // union, consider cases without ()
consume()
type = {
...parseType(true, []),
...type, // preserve nullable
}
if (peek() != ')') throw new Error('Expecting closing )')
consume()
if (peek() != '|') {
return type
}
} else if (token == '{') {
consume()
type.record = parseRecord()
return type
}
if ([nonNullable, nullable].includes(token))
throw new Error('Nullability already defined.')
if (/[=),:.<>}|]/.test(token))
throw new Error(`Unexpected token ${token}.`)
if (peek() != '|') {
type.name = peek()
consume()
}
if (fn == token) {
type.function = parseFunction()
} else if (
peek() == '<' ||
(applicationWithDot = (peek() == '.' && peekNext() == '<'))
) {
consume()
if (applicationWithDot) consume()
type.application = parseApplication()
}
while(peek() == '.') {
type.name += '.'
consume()
const { name } = parseType(false)
if (!name) throw new Error('Expected to see the name after .')
type.name += name
}
if (peek() != '|' || !specialAllowed) return type
union.push(type)
while(peek() == '|') {
consume()
const nextType = parseType(true, union)
// const u = nextType.union ? nextType.union : [nextType]
if(nextType.union !== union) union.push(nextType)
}
/** @type {!_typedefsParser.Type} */
const unionType = { union }
return unionType
}
return parseType()
}
/**
* @suppress {nonStandardJsDocs}
* @typedef {import('.').Type} _typedefsParser.Type
*/
/**
* @suppress {nonStandardJsDocs}
* @typedef {import('.').FunctionType} _typedefsParser.FunctionType
*/
module.exports.lex = lex
module.exports.parser = parser