nixos/lua-lsp/script/parser/luadoc.lua

2071 lines
57 KiB
Lua
Raw Normal View History

local m = require 'lpeglabel'
local re = require 'parser.relabel'
local guide = require 'parser.guide'
local compile = require 'parser.compile'
local util = require 'utility'
local TokenTypes, TokenStarts, TokenFinishs, TokenContents, TokenMarks
---@type integer
local Ci
---@type integer
local Offset
local pushWarning, NextComment, Lines
local parseType, parseTypeUnit
---@type any
local Parser = re.compile([[
Main <- (Token / Sp)*
Sp <- %s+
X16 <- [a-fA-F0-9]
Token <- Integer / Name / String / Code / Symbol
Name <- ({} {%name} {})
-> Name
Integer <- ({} {'-'? [0-9]+} !'.' {})
-> Integer
Code <- ({} '`' { (!'`' .)*} '`' {})
-> Code
String <- ({} StringDef {})
-> String
StringDef <- {'"'}
{~(Esc / !'"' .)*~} -> 1
('"'?)
/ {"'"}
{~(Esc / !"'" .)*~} -> 1
("'"?)
/ '[' {:eq: '='* :} '['
=eq -> LongStringMark
{(!StringClose .)*} -> 1
StringClose?
StringClose <- ']' =eq ']'
Esc <- '\' -> ''
EChar
EChar <- 'a' -> ea
/ 'b' -> eb
/ 'f' -> ef
/ 'n' -> en
/ 'r' -> er
/ 't' -> et
/ 'v' -> ev
/ '\'
/ '"'
/ "'"
/ %nl
/ ('z' (%nl / %s)*) -> ''
/ ('x' {X16 X16}) -> Char16
/ ([0-9] [0-9]? [0-9]?) -> Char10
/ ('u{' {X16*} '}') -> CharUtf8
Symbol <- ({} {
[:|,;<>()?+#{}]
/ '[]'
/ '...'
/ '['
/ ']'
/ '-' !'-'
} {})
-> Symbol
]], {
s = m.S' \t\v\f',
ea = '\a',
eb = '\b',
ef = '\f',
en = '\n',
er = '\r',
et = '\t',
ev = '\v',
name = (m.R('az', 'AZ', '09', '\x80\xff') + m.S('_')) * (m.R('az', 'AZ', '__', '09', '\x80\xff') + m.S('_.*-'))^0,
Char10 = function (char)
---@type integer?
char = tonumber(char)
if not char or char < 0 or char > 255 then
return ''
end
return string.char(char)
end,
Char16 = function (char)
return string.char(tonumber(char, 16))
end,
CharUtf8 = function (char)
if #char == 0 then
return ''
end
local v = tonumber(char, 16)
if not v then
return ''
end
if v >= 0 and v <= 0x10FFFF then
return utf8.char(v)
end
return ''
end,
LongStringMark = function (back)
return '[' .. back .. '['
end,
Name = function (start, content, finish)
Ci = Ci + 1
TokenTypes[Ci] = 'name'
TokenStarts[Ci] = start
TokenFinishs[Ci] = finish - 1
TokenContents[Ci] = content
end,
String = function (start, mark, content, finish)
Ci = Ci + 1
TokenTypes[Ci] = 'string'
TokenStarts[Ci] = start
TokenFinishs[Ci] = finish - 1
TokenContents[Ci] = content
TokenMarks[Ci] = mark
end,
Integer = function (start, content, finish)
Ci = Ci + 1
TokenTypes[Ci] = 'integer'
TokenStarts[Ci] = start
TokenFinishs[Ci] = finish - 1
TokenContents[Ci] = math.tointeger(content)
end,
Code = function (start, content, finish)
Ci = Ci + 1
TokenTypes[Ci] = 'code'
TokenStarts[Ci] = start
TokenFinishs[Ci] = finish - 1
TokenContents[Ci] = content
end,
Symbol = function (start, content, finish)
Ci = Ci + 1
TokenTypes[Ci] = 'symbol'
TokenStarts[Ci] = start
TokenFinishs[Ci] = finish - 1
TokenContents[Ci] = content
end,
})
---@alias parser.visibleType 'public' | 'protected' | 'private' | 'package'
---@class parser.object
---@field literal boolean
---@field signs parser.object[]
---@field originalComment parser.object
---@field as? parser.object
---@field touch? integer
---@field module? string
---@field async? boolean
---@field versions? table[]
---@field names? parser.object[]
---@field path? string
---@field bindComments? parser.object[]
---@field visible? parser.visibleType
---@field operators? parser.object[]
---@field calls? parser.object[]
---@field generics? parser.object[]
---@field generic? parser.object
local function parseTokens(text, offset)
Ci = 0
Offset = offset
TokenTypes = {}
TokenStarts = {}
TokenFinishs = {}
TokenContents = {}
TokenMarks = {}
Parser:match(text)
Ci = 0
end
local function peekToken(offset)
offset = offset or 1
return TokenTypes[Ci + offset], TokenContents[Ci + offset]
end
---@return string? tokenType
---@return string? tokenContent
local function nextToken()
Ci = Ci + 1
if not TokenTypes[Ci] then
Ci = Ci - 1
return nil, nil
end
return TokenTypes[Ci], TokenContents[Ci]
end
local function checkToken(tp, content, offset)
offset = offset or 0
return TokenTypes[Ci + offset] == tp
and TokenContents[Ci + offset] == content
end
local function getStart()
if Ci == 0 then
return Offset
end
return TokenStarts[Ci] + Offset
end
---@return integer
local function getFinish()
if Ci == 0 then
return Offset
end
return TokenFinishs[Ci] + Offset + 1
end
local function getMark()
return TokenMarks[Ci]
end
local function try(callback)
local savePoint = Ci
-- rollback
local suc = callback()
if not suc then
Ci = savePoint
end
return suc
end
local function parseName(tp, parent)
local nameTp, nameText = peekToken()
if nameTp ~= 'name' then
return nil
end
nextToken()
local name = {
type = tp,
start = getStart(),
finish = getFinish(),
parent = parent,
[1] = nameText,
}
return name
end
local function nextSymbolOrError(symbol)
if checkToken('symbol', symbol, 1) then
nextToken()
return true
end
pushWarning {
type = 'LUADOC_MISS_SYMBOL',
start = getFinish(),
finish = getFinish(),
info = {
symbol = symbol,
}
}
return false
end
local function parseIndexField(parent)
if not checkToken('symbol', '[', 1) then
return nil
end
nextToken()
local field = parseType(parent)
nextSymbolOrError ']'
return field
end
local function parseTable(parent)
if not checkToken('symbol', '{', 1) then
return nil
end
nextToken()
local typeUnit = {
type = 'doc.type.table',
start = getStart(),
parent = parent,
fields = {},
}
while true do
if checkToken('symbol', '}', 1) then
nextToken()
break
end
local field = {
type = 'doc.type.field',
parent = typeUnit,
}
do
local needCloseParen
if checkToken('symbol', '(', 1) then
nextToken()
needCloseParen = true
end
field.name = parseName('doc.field.name', field)
or parseIndexField(field)
if not field.name then
pushWarning {
type = 'LUADOC_MISS_FIELD_NAME',
start = getFinish(),
finish = getFinish(),
}
break
end
if not field.start then
field.start = field.name.start
end
if checkToken('symbol', '?', 1) then
nextToken()
field.optional = true
end
field.finish = getFinish()
if not nextSymbolOrError(':') then
break
end
field.extends = parseType(field)
if not field.extends then
break
end
field.finish = getFinish()
if needCloseParen then
nextSymbolOrError ')'
end
end
typeUnit.fields[#typeUnit.fields+1] = field
if checkToken('symbol', ',', 1)
or checkToken('symbol', ';', 1) then
nextToken()
else
nextSymbolOrError('}')
break
end
end
typeUnit.finish = getFinish()
return typeUnit
end
local function parseSigns(parent)
if not checkToken('symbol', '<', 1) then
return nil
end
nextToken()
local signs = {}
while true do
local sign = parseName('doc.generic.name', parent)
if not sign then
pushWarning {
type = 'LUADOC_MISS_SIGN_NAME',
start = getFinish(),
finish = getFinish(),
}
break
end
signs[#signs+1] = sign
if checkToken('symbol', ',', 1) then
nextToken()
else
break
end
end
nextSymbolOrError '>'
return signs
end
local function parseDots(tp, parent)
if not checkToken('symbol', '...', 1) then
return
end
nextToken()
local dots = {
type = tp,
start = getStart(),
finish = getFinish(),
parent = parent,
[1] = '...',
}
return dots
end
local function parseTypeUnitFunction(parent)
if not checkToken('name', 'fun', 1) then
return nil
end
nextToken()
local typeUnit = {
type = 'doc.type.function',
parent = parent,
start = getStart(),
args = {},
returns = {},
}
if not nextSymbolOrError('(') then
return nil
end
while true do
if checkToken('symbol', ')', 1) then
nextToken()
break
end
local arg = {
type = 'doc.type.arg',
parent = typeUnit,
}
arg.name = parseName('doc.type.arg.name', arg)
or parseDots('doc.type.arg.name', arg)
if not arg.name then
pushWarning {
type = 'LUADOC_MISS_ARG_NAME',
start = getFinish(),
finish = getFinish(),
}
break
end
if not arg.start then
arg.start = arg.name.start
end
if checkToken('symbol', '?', 1) then
nextToken()
arg.optional = true
end
arg.finish = getFinish()
if checkToken('symbol', ':', 1) then
nextToken()
arg.extends = parseType(arg)
end
arg.finish = getFinish()
typeUnit.args[#typeUnit.args+1] = arg
if checkToken('symbol', ',', 1) then
nextToken()
else
nextSymbolOrError(')')
break
end
end
if checkToken('symbol', ':', 1) then
nextToken()
local needCloseParen
if checkToken('symbol', '(', 1) then
nextToken()
needCloseParen = true
end
while true do
local name
try(function ()
local returnName = parseName('doc.return.name', typeUnit)
or parseDots('doc.return.name', typeUnit)
if not returnName then
return false
end
if checkToken('symbol', ':', 1) then
nextToken()
name = returnName
return true
end
if returnName[1] == '...' then
name = returnName
return false
end
return false
end)
local rtn = parseType(typeUnit)
if not rtn then
break
end
rtn.name = name
if checkToken('symbol', '?', 1) then
nextToken()
rtn.optional = true
end
typeUnit.returns[#typeUnit.returns+1] = rtn
if checkToken('symbol', ',', 1) then
nextToken()
else
break
end
end
if needCloseParen then
nextSymbolOrError ')'
end
end
typeUnit.finish = getFinish()
return typeUnit
end
local function parseFunction(parent)
local _, content = peekToken()
if content == 'async' then
nextToken()
local pos = getStart()
local tp, cont = peekToken()
if tp == 'name' then
if cont == 'fun' then
local func = parseTypeUnit(parent)
if func then
func.async = true
func.asyncPos = pos
return func
end
end
end
end
if content == 'fun' then
return parseTypeUnitFunction(parent)
end
end
local function parseTypeUnitArray(parent, node)
if not checkToken('symbol', '[]', 1) then
return nil
end
nextToken()
local result = {
type = 'doc.type.array',
start = node.start,
finish = getFinish(),
node = node,
parent = parent,
}
node.parent = result
return result
end
local function parseTypeUnitSign(parent, node)
if not checkToken('symbol', '<', 1) then
return nil
end
nextToken()
local result = {
type = 'doc.type.sign',
start = node.start,
finish = getFinish(),
node = node,
parent = parent,
signs = {},
}
node.parent = result
while true do
local sign = parseType(result)
if not sign then
pushWarning {
type = 'LUA_DOC_MISS_SIGN',
start = getFinish(),
finish = getFinish(),
}
break
end
result.signs[#result.signs+1] = sign
if checkToken('symbol', ',', 1) then
nextToken()
else
break
end
end
nextSymbolOrError '>'
result.finish = getFinish()
return result
end
local function parseString(parent)
local tp, content = peekToken()
if not tp or tp ~= 'string' then
return nil
end
nextToken()
local mark = getMark()
-- compatibility
if content:sub(1, 1) == '"'
or content:sub(1, 1) == "'" then
if content:sub(1, 1) == content:sub(-1, -1) then
mark = content:sub(1, 1)
content = content:sub(2, -2)
end
end
local str = {
type = 'doc.type.string',
start = getStart(),
finish = getFinish(),
parent = parent,
[1] = content,
[2] = mark,
}
return str
end
local function parseCode(parent)
local tp, content = peekToken()
if not tp or tp ~= 'code' then
return nil
end
nextToken()
local code = {
type = 'doc.type.code',
start = getStart(),
finish = getFinish(),
parent = parent,
[1] = content,
}
return code
end
local function parseInteger(parent)
local tp, content = peekToken()
if not tp or tp ~= 'integer' then
return nil
end
nextToken()
local integer = {
type = 'doc.type.integer',
start = getStart(),
finish = getFinish(),
parent = parent,
[1] = content,
}
return integer
end
local function parseBoolean(parent)
local tp, content = peekToken()
if not tp
or tp ~= 'name'
or (content ~= 'true' and content ~= 'false') then
return nil
end
nextToken()
local boolean = {
type = 'doc.type.boolean',
start = getStart(),
finish = getFinish(),
parent = parent,
[1] = content == 'true' and true or false,
}
return boolean
end
local function parseParen(parent)
if not checkToken('symbol', '(', 1) then
return
end
nextToken()
local tp = parseType(parent)
nextSymbolOrError(')')
return tp
end
function parseTypeUnit(parent)
local result = parseFunction(parent)
or parseTable(parent)
or parseString(parent)
or parseCode(parent)
or parseInteger(parent)
or parseBoolean(parent)
or parseParen(parent)
if not result then
result = parseName('doc.type.name', parent)
or parseDots('doc.type.name', parent)
if not result then
return nil
end
if result[1] == '...' then
result[1] = 'unknown'
end
end
while true do
local newResult = parseTypeUnitSign(parent, result)
if not newResult then
break
end
result = newResult
end
while true do
local newResult = parseTypeUnitArray(parent, result)
if not newResult then
break
end
result = newResult
end
return result
end
local function parseResume(parent)
local default, additional
if checkToken('symbol', '>', 1) then
nextToken()
default = true
end
if checkToken('symbol', '+', 1) then
nextToken()
additional = true
end
local result = parseTypeUnit(parent)
if result then
result.default = default
result.additional = additional
end
return result
end
function parseType(parent)
local result = {
type = 'doc.type',
parent = parent,
types = {},
}
while true do
local typeUnit = parseTypeUnit(result)
if not typeUnit then
break
end
result.types[#result.types+1] = typeUnit
if not result.start then
result.start = typeUnit.start
end
if not checkToken('symbol', '|', 1) then
break
end
nextToken()
end
if not result.start then
result.start = getFinish()
end
if checkToken('symbol', '?', 1) then
nextToken()
result.optional = true
end
result.finish = getFinish()
result.firstFinish = result.finish
local row = guide.rowColOf(result.finish)
local function pushResume()
local comments
for i = 0, 100 do
local nextComm = NextComment(i,'peek')
if not nextComm then
return false
end
local nextCommRow = guide.rowColOf(nextComm.start)
local currentRow = row + i + 1
if currentRow < nextCommRow then
return false
end
if nextComm.text:match '^%-%s*%@' then
return false
else
local resumeHead = nextComm.text:match '^%-%s*%|'
if resumeHead then
NextComment(i)
row = row + i + 1
local finishPos = nextComm.text:find('#', #resumeHead + 1) or #nextComm.text
parseTokens(nextComm.text:sub(#resumeHead + 1, finishPos), nextComm.start + #resumeHead + 1)
local resume = parseResume(result)
if resume then
if comments then
resume.comment = table.concat(comments, '\n')
else
resume.comment = nextComm.text:match('%s*#?%s*(.+)', resume.finish - nextComm.start)
end
result.types[#result.types+1] = resume
result.finish = resume.finish
end
comments = nil
return true
else
if not comments then
comments = {}
end
comments[#comments+1] = nextComm.text:sub(2)
end
end
end
return false
end
local checkResume = true
local nsymbol, ncontent = peekToken()
if nsymbol == 'symbol' then
if ncontent == ','
or ncontent == ':'
or ncontent == '|'
or ncontent == ')'
or ncontent == '}' then
checkResume = false
end
end
if checkResume then
while pushResume() do end
end
if #result.types == 0 then
pushWarning {
type = 'LUADOC_MISS_TYPE_NAME',
start = getFinish(),
finish = getFinish(),
}
return nil
end
return result
end
local docSwitch = util.switch()
: case 'class'
: call(function ()
local result = {
type = 'doc.class',
fields = {},
operators = {},
calls = {},
}
result.class = parseName('doc.class.name', result)
if not result.class then
pushWarning {
type = 'LUADOC_MISS_CLASS_NAME',
start = getFinish(),
finish = getFinish(),
}
return nil
end
result.start = getStart()
result.finish = getFinish()
result.signs = parseSigns(result)
if not checkToken('symbol', ':', 1) then
return result
end
nextToken()
result.extends = {}
while true do
local extend = parseName('doc.extends.name', result)
or parseTable(result)
if not extend then
pushWarning {
type = 'LUADOC_MISS_CLASS_EXTENDS_NAME',
start = getFinish(),
finish = getFinish(),
}
return result
end
result.extends[#result.extends+1] = extend
result.finish = getFinish()
if not checkToken('symbol', ',', 1) then
break
end
nextToken()
end
return result
end)
: case 'type'
: call(function ()
local first = parseType()
if not first then
return nil
end
local rests
while checkToken('symbol', ',', 1) do
nextToken()
local rest = parseType()
if not rests then
rests = {}
end
rests[#rests+1] = rest
end
return first, rests
end)
: case 'alias'
: call(function ()
local result = {
type = 'doc.alias',
}
result.alias = parseName('doc.alias.name', result)
if not result.alias then
pushWarning {
type = 'LUADOC_MISS_ALIAS_NAME',
start = getFinish(),
finish = getFinish(),
}
return nil
end
result.start = getStart()
result.signs = parseSigns(result)
result.extends = parseType(result)
if not result.extends then
pushWarning {
type = 'LUADOC_MISS_ALIAS_EXTENDS',
start = getFinish(),
finish = getFinish(),
}
return nil
end
result.finish = getFinish()
return result
end)
: case 'param'
: call(function ()
local result = {
type = 'doc.param',
}
result.param = parseName('doc.param.name', result)
or parseDots('doc.param.name', result)
if not result.param then
pushWarning {
type = 'LUADOC_MISS_PARAM_NAME',
start = getFinish(),
finish = getFinish(),
}
return nil
end
if checkToken('symbol', '?', 1) then
nextToken()
result.optional = true
end
result.start = result.param.start
result.finish = getFinish()
result.extends = parseType(result)
if not result.extends then
pushWarning {
type = 'LUADOC_MISS_PARAM_EXTENDS',
start = getFinish(),
finish = getFinish(),
}
return result
end
result.finish = getFinish()
result.firstFinish = result.extends.firstFinish
return result
end)
: case 'return'
: call(function ()
local result = {
type = 'doc.return',
returns = {},
}
while true do
local dots = parseDots('doc.return.name')
if dots then
Ci = Ci - 1
end
local docType = parseType(result)
if not docType then
break
end
if not result.start then
result.start = docType.start
end
if checkToken('symbol', '?', 1) then
nextToken()
docType.optional = true
end
if dots then
docType.name = dots
dots.parent = docType
else
docType.name = parseName('doc.return.name', docType)
or parseDots('doc.return.name', docType)
end
result.returns[#result.returns+1] = docType
if not checkToken('symbol', ',', 1) then
break
end
nextToken()
end
if #result.returns == 0 then
return nil
end
result.finish = getFinish()
return result
end)
: case 'field'
: call(function ()
local result = {
type = 'doc.field',
}
try(function ()
local tp, value = nextToken()
if tp == 'name' then
if value == 'public'
or value == 'protected'
or value == 'private'
or value == 'package' then
local tp2 = peekToken(1)
local tp3 = peekToken(2)
if tp2 == 'name' and not tp3 then
return false
end
result.visible = value
result.start = getStart()
return true
end
end
return false
end)
result.field = parseName('doc.field.name', result)
or parseIndexField(result)
if not result.field then
pushWarning {
type = 'LUADOC_MISS_FIELD_NAME',
start = getFinish(),
finish = getFinish(),
}
return nil
end
if not result.start then
result.start = result.field.start
end
if checkToken('symbol', '?', 1) then
nextToken()
result.optional = true
end
result.extends = parseType(result)
if not result.extends then
pushWarning {
type = 'LUADOC_MISS_FIELD_EXTENDS',
start = getFinish(),
finish = getFinish(),
}
return nil
end
result.finish = getFinish()
return result
end)
: case 'generic'
: call(function ()
local result = {
type = 'doc.generic',
generics = {},
}
while true do
local object = {
type = 'doc.generic.object',
parent = result,
}
object.generic = parseName('doc.generic.name', object)
if not object.generic then
pushWarning {
type = 'LUADOC_MISS_GENERIC_NAME',
start = getFinish(),
finish = getFinish(),
}
return nil
end
object.start = object.generic.start
if not result.start then
result.start = object.start
end
if checkToken('symbol', ':', 1) then
nextToken()
object.extends = parseType(object)
end
object.finish = getFinish()
result.generics[#result.generics+1] = object
if not checkToken('symbol', ',', 1) then
break
end
nextToken()
end
result.finish = getFinish()
return result
end)
: case 'vararg'
: call(function ()
local result = {
type = 'doc.vararg',
}
result.vararg = parseType(result)
if not result.vararg then
pushWarning {
type = 'LUADOC_MISS_VARARG_TYPE',
start = getFinish(),
finish = getFinish(),
}
return
end
result.start = result.vararg.start
result.finish = result.vararg.finish
return result
end)
: case 'overload'
: call(function ()
local tp, name = peekToken()
if tp ~= 'name'
or (name ~= 'fun' and name ~= 'async') then
pushWarning {
type = 'LUADOC_MISS_FUN_AFTER_OVERLOAD',
start = getFinish(),
finish = getFinish(),
}
return nil
end
local result = {
type = 'doc.overload',
}
result.overload = parseFunction(result)
if not result.overload then
return nil
end
result.overload.parent = result
result.start = result.overload.start
result.finish = result.overload.finish
return result
end)
: case 'deprecated'
: call(function ()
return {
type = 'doc.deprecated',
start = getFinish(),
finish = getFinish(),
}
end)
: case 'meta'
: call(function ()
return {
type = 'doc.meta',
start = getFinish(),
finish = getFinish(),
}
end)
: case 'version'
: call(function ()
local result = {
type = 'doc.version',
versions = {},
}
while true do
local tp, text = nextToken()
if not tp then
pushWarning {
type = 'LUADOC_MISS_VERSION',
start = getFinish(),
finish = getFinish(),
}
break
end
if not result.start then
result.start = getStart()
end
local version = {
type = 'doc.version.unit',
parent = result,
start = getStart(),
}
if tp == 'symbol' then
if text == '>' then
version.ge = true
elseif text == '<' then
version.le = true
end
tp, text = nextToken()
end
if tp ~= 'name' then
pushWarning {
type = 'LUADOC_MISS_VERSION',
start = getStart(),
finish = getFinish(),
}
break
end
version.version = tonumber(text) or text
version.finish = getFinish()
result.versions[#result.versions+1] = version
if not checkToken('symbol', ',', 1) then
break
end
nextToken()
end
if #result.versions == 0 then
return nil
end
result.finish = getFinish()
return result
end)
: case 'see'
: call(function ()
local result = {
type = 'doc.see',
}
result.name = parseName('doc.see.name', result)
if not result.name then
pushWarning {
type = 'LUADOC_MISS_SEE_NAME',
start = getFinish(),
finish = getFinish(),
}
return nil
end
result.start = result.name.start
result.finish = result.name.finish
return result
end)
: case 'diagnostic'
: call(function ()
local result = {
type = 'doc.diagnostic',
}
local nextTP, mode = nextToken()
if nextTP ~= 'name' then
pushWarning {
type = 'LUADOC_MISS_DIAG_MODE',
start = getFinish(),
finish = getFinish(),
}
return nil
end
result.mode = mode
result.start = getStart()
result.finish = getFinish()
if mode ~= 'disable-next-line'
and mode ~= 'disable-line'
and mode ~= 'disable'
and mode ~= 'enable' then
pushWarning {
type = 'LUADOC_ERROR_DIAG_MODE',
start = result.start,
finish = result.finish,
}
end
if checkToken('symbol', ':', 1) then
nextToken()
result.names = {}
while true do
local name = parseName('doc.diagnostic.name', result)
if not name then
pushWarning {
type = 'LUADOC_MISS_DIAG_NAME',
start = getFinish(),
finish = getFinish(),
}
return result
end
result.names[#result.names+1] = name
if not checkToken('symbol', ',', 1) then
break
end
nextToken()
end
end
result.finish = getFinish()
return result
end)
: case 'module'
: call(function ()
local result = {
type = 'doc.module',
start = getFinish(),
finish = getFinish(),
}
local tp, content = peekToken()
if tp == 'string' then
result.module = content
nextToken()
result.start = getStart()
result.finish = getFinish()
result.smark = getMark()
else
pushWarning {
type = 'LUADOC_MISS_MODULE_NAME',
start = getFinish(),
finish = getFinish(),
}
end
return result
end)
: case 'async'
: call(function ()
return {
type = 'doc.async',
start = getFinish(),
finish = getFinish(),
}
end)
: case 'nodiscard'
: call(function ()
return {
type = 'doc.nodiscard',
start = getFinish(),
finish = getFinish(),
}
end)
: case 'as'
: call(function ()
local result = {
type = 'doc.as',
start = getFinish(),
finish = getFinish(),
}
result.as = parseType(result)
result.finish = getFinish()
return result
end)
: case 'cast'
: call(function ()
local result = {
type = 'doc.cast',
start = getFinish(),
finish = getFinish(),
casts = {},
}
local loc = parseName('doc.cast.name', result)
if not loc then
pushWarning {
type = 'LUADOC_MISS_LOCAL_NAME',
start = getFinish(),
finish = getFinish(),
}
return result
end
result.loc = loc
result.finish = loc.finish
while true do
local block = {
type = 'doc.cast.block',
parent = result,
start = getFinish(),
finish = getFinish(),
}
if checkToken('symbol', '+', 1) then
block.mode = '+'
nextToken()
block.start = getStart()
block.finish = getFinish()
elseif checkToken('symbol', '-', 1) then
block.mode = '-'
nextToken()
block.start = getStart()
block.finish = getFinish()
end
if checkToken('symbol', '?', 1) then
block.optional = true
nextToken()
block.finish = getFinish()
else
block.extends = parseType(block)
if block.extends then
block.start = block.start or block.extends.start
block.finish = block.extends.finish
end
end
if block.optional or block.extends then
result.casts[#result.casts+1] = block
end
result.finish = block.finish
if checkToken('symbol', ',', 1) then
nextToken()
else
break
end
end
return result
end)
: case 'operator'
: call(function ()
local result = {
type = 'doc.operator',
start = getFinish(),
finish = getFinish(),
}
local op = parseName('doc.operator.name', result)
if not op then
pushWarning {
type = 'LUADOC_MISS_OPERATOR_NAME',
start = getFinish(),
finish = getFinish(),
}
return nil
end
result.op = op
result.finish = op.finish
if checkToken('symbol', '(', 1) then
nextToken()
if checkToken('symbol', ')', 1) then
nextToken()
else
local exp = parseType(result)
if exp then
result.exp = exp
result.finish = exp.finish
end
nextSymbolOrError ')'
end
end
nextSymbolOrError ':'
local ret = parseType(result)
if ret then
result.extends = ret
result.finish = ret.finish
end
return result
end)
: case 'source'
: call(function (doc)
local fullSource = doc:sub(#'source' + 1)
if not fullSource or fullSource == '' then
return
end
fullSource = util.trim(fullSource)
if fullSource == '' then
return
end
local source, line, char = fullSource:match('^(.-):?(%d*):?(%d*)$')
source = source or fullSource
line = tonumber(line) or 1
char = tonumber(char) or 0
local result = {
type = 'doc.source',
start = getStart(),
finish = getFinish(),
path = source,
line = line,
char = char,
}
return result
end)
: case 'enum'
: call(function ()
local name = parseName('doc.enum.name')
if not name then
return nil
end
local result = {
type = 'doc.enum',
start = name.start,
finish = name.finish,
enum = name,
}
name.parent = result
return result
end)
: case 'private'
: call(function ()
return {
type = 'doc.private',
start = getFinish(),
finish = getFinish(),
}
end)
: case 'protected'
: call(function ()
return {
type = 'doc.protected',
start = getFinish(),
finish = getFinish(),
}
end)
: case 'public'
: call(function ()
return {
type = 'doc.public',
start = getFinish(),
finish = getFinish(),
}
end)
: case 'package'
: call(function ()
return {
type = 'doc.package',
start = getFinish(),
finish = getFinish(),
}
end)
local function convertTokens(doc)
local tp, text = nextToken()
if not tp then
return
end
if tp ~= 'name' then
pushWarning {
type = 'LUADOC_MISS_CATE_NAME',
start = getStart(),
finish = getFinish(),
}
return nil
end
return docSwitch(text, doc)
end
local function trimTailComment(text)
local comment = text
if text:sub(1, 1) == '@' then
comment = text:sub(2)
end
if text:sub(1, 1) == '#' then
comment = text:sub(2)
end
if text:sub(1, 2) == '--' then
comment = text:sub(3)
end
if comment:find '^%s*[\'"[]' then
local state = compile(comment:gsub('^%s+', ''), 'String')
if state and state.ast then
comment = state.ast[1]
end
end
return comment
end
local function buildLuaDoc(comment)
local text = comment.text
local startPos = (comment.type == 'comment.short' and text:match '^%-%s*@()')
or (comment.type == 'comment.long' and text:match '^@()')
if not startPos then
return {
type = 'doc.comment',
start = comment.start,
finish = comment.finish,
range = comment.finish,
comment = comment,
}
end
local startOffset = comment.start
if comment.type == 'comment.long' then
startOffset = startOffset + #comment.mark - 2
end
local doc = text:sub(startPos)
parseTokens(doc, startOffset + startPos)
local result, rests = convertTokens(doc)
if result then
result.range = comment.finish
local finish = result.firstFinish or result.finish
if rests then
for _, rest in ipairs(rests) do
rest.range = comment.finish
finish = rest.firstFinish or result.finish
end
end
local cstart = text:find('%S', finish - comment.start)
if cstart and cstart < comment.finish then
result.comment = {
type = 'doc.tailcomment',
start = cstart + comment.start,
finish = comment.finish,
parent = result,
text = trimTailComment(text:sub(cstart)),
}
if rests then
for _, rest in ipairs(rests) do
rest.comment = result.comment
end
end
end
end
if result then
return result, rests
end
return {
type = 'doc.comment',
start = comment.start,
finish = comment.finish,
range = comment.finish,
comment = comment,
}
end
local function isTailComment(text, doc)
if not doc then
return false
end
local left = doc.originalComment.start
local row, col = guide.rowColOf(left)
local lineStart = Lines[row] or 0
local hasCodeBefore = text:sub(lineStart, lineStart + col):find '[%w_]'
return hasCodeBefore
end
local function isContinuedDoc(lastDoc, nextDoc)
if not nextDoc then
return false
end
if nextDoc.type == 'doc.diagnostic' then
return true
end
if lastDoc.type == 'doc.type'
or lastDoc.type == 'doc.module'
or lastDoc.type == 'doc.enum' then
if nextDoc.type ~= 'doc.comment' then
return false
end
end
if lastDoc.type == 'doc.class'
or lastDoc.type == 'doc.field'
or lastDoc.type == 'doc.operator' then
if nextDoc.type ~= 'doc.field'
and nextDoc.type ~= 'doc.operator'
and nextDoc.type ~= 'doc.comment'
and nextDoc.type ~= 'doc.overload'
and nextDoc.type ~= 'doc.source' then
return false
end
end
if nextDoc.type == 'doc.cast' then
return false
end
return true
end
local function isNextLine(lastDoc, nextDoc)
if not nextDoc then
return false
end
local lastRow = guide.rowColOf(lastDoc.finish)
local newRow = guide.rowColOf(nextDoc.start)
return newRow - lastRow == 1
end
local function bindGeneric(binded)
local generics = {}
for _, doc in ipairs(binded) do
if doc.type == 'doc.generic' then
for _, obj in ipairs(doc.generics) do
local name = obj.generic[1]
generics[name] = obj
end
end
if doc.type == 'doc.class'
or doc.type == 'doc.alias' then
if doc.signs then
for _, sign in ipairs(doc.signs) do
local name = sign[1]
generics[name] = sign
end
end
end
if doc.type == 'doc.param'
or doc.type == 'doc.vararg'
or doc.type == 'doc.return'
or doc.type == 'doc.type'
or doc.type == 'doc.class'
or doc.type == 'doc.alias' then
guide.eachSourceType(doc, 'doc.type.name', function (src)
local name = src[1]
if generics[name] then
src.type = 'doc.generic.name'
src.generic = generics[name]
end
end)
guide.eachSourceType(doc, 'doc.type.code', function (src)
local name = src[1]
if generics[name] then
src.type = 'doc.generic.name'
src.literal = true
end
end)
end
end
end
local function bindDocWithSource(doc, source)
if not source.bindDocs then
source.bindDocs = {}
end
source.bindDocs[#source.bindDocs+1] = doc
doc.bindSource = source
end
local function bindDoc(source, binded)
local isParam = source.type == 'self'
or source.type == 'local'
and (source.parent.type == 'funcargs'
or ( source.parent.type == 'in'
and source.finish <= source.parent.keys.finish
)
)
local ok = false
for _, doc in ipairs(binded) do
if doc.bindSource then
goto CONTINUE
end
if doc.type == 'doc.class'
or doc.type == 'doc.deprecated'
or doc.type == 'doc.version'
or doc.type == 'doc.module'
or doc.type == 'doc.source'
or doc.type == 'doc.private'
or doc.type == 'doc.protected'
or doc.type == 'doc.public'
or doc.type == 'doc.package'
or doc.type == 'doc.see' then
if source.type == 'function'
or isParam then
goto CONTINUE
end
bindDocWithSource(doc, source)
ok = true
elseif doc.type == 'doc.type' then
if source.type == 'function'
or isParam
or source._bindedDocType then
goto CONTINUE
end
source._bindedDocType = true
bindDocWithSource(doc, source)
ok = true
elseif doc.type == 'doc.overload' then
if not source.bindDocs then
source.bindDocs = {}
end
source.bindDocs[#source.bindDocs+1] = doc
if source.type == 'function' then
bindDocWithSource(doc, source)
end
ok = true
elseif doc.type == 'doc.param' then
if isParam
and doc.param[1] == source[1] then
bindDocWithSource(doc, source)
ok = true
elseif source.type == '...'
and doc.param[1] == '...' then
bindDocWithSource(doc, source)
ok = true
elseif source.type == 'self'
and doc.param[1] == 'self' then
bindDocWithSource(doc, source)
ok = true
elseif source.type == 'function' then
if not source.bindDocs then
source.bindDocs = {}
end
source.bindDocs[#source.bindDocs + 1] = doc
if source.args then
for _, arg in ipairs(source.args) do
if arg[1] == doc.param[1] then
bindDocWithSource(doc, arg)
break
end
end
end
end
elseif doc.type == 'doc.vararg' then
if source.type == '...' then
bindDocWithSource(doc, source)
ok = true
end
elseif doc.type == 'doc.return'
or doc.type == 'doc.generic'
or doc.type == 'doc.async'
or doc.type == 'doc.nodiscard' then
if source.type == 'function' then
bindDocWithSource(doc, source)
ok = true
end
elseif doc.type == 'doc.enum' then
if source.type == 'table' then
bindDocWithSource(doc, source)
ok = true
end
if source.value and source.value.type == 'table' then
bindDocWithSource(doc, source.value)
goto CONTINUE
end
elseif doc.type == 'doc.comment' then
bindDocWithSource(doc, source)
ok = true
end
::CONTINUE::
end
return ok
end
local function bindDocsBetween(sources, binded, start, finish)
-- 用二分法找到第一个
local max = #sources
local index
local left = 1
local right = max
for _ = 1, 1000 do
index = left + (right - left) // 2
if index <= left then
index = left
break
elseif index >= right then
index = right
break
end
local src = sources[index]
if src.start < start then
left = index + 1
else
right = index
end
end
local ok = false
-- 从前往后进行绑定
for i = index, max do
local src = sources[i]
if src and src.start >= start then
if src.start >= finish then
break
end
if src.start >= start then
if src.type == 'local'
or src.type == 'self'
or src.type == 'setlocal'
or src.type == 'setglobal'
or src.type == 'tablefield'
or src.type == 'tableindex'
or src.type == 'setfield'
or src.type == 'setindex'
or src.type == 'setmethod'
or src.type == 'function'
or src.type == 'table'
or src.type == '...' then
if bindDoc(src, binded) then
ok = true
end
end
end
end
end
return ok
end
local function bindReturnIndex(binded)
local returnIndex = 0
for _, doc in ipairs(binded) do
if doc.type == 'doc.return' then
for _, rtn in ipairs(doc.returns) do
returnIndex = returnIndex + 1
rtn.returnIndex = returnIndex
end
end
end
end
local function bindCommentsToDoc(doc, comments)
doc.bindComments = comments
for _, comment in ipairs(comments) do
comment.bindSource = doc
end
end
local function bindCommentsAndFields(binded)
local class
local comments = {}
local source
for _, doc in ipairs(binded) do
if doc.type == 'doc.class' then
-- 多个class连续写在一起只有最后一个class可以绑定source
if class then
class.bindSource = nil
end
if source then
doc.source = source
source.bindSource = doc
end
class = doc
bindCommentsToDoc(doc, comments)
comments = {}
elseif doc.type == 'doc.field' then
if class then
class.fields[#class.fields+1] = doc
doc.class = class
end
if source then
doc.source = source
source.bindSource = doc
end
bindCommentsToDoc(doc, comments)
comments = {}
elseif doc.type == 'doc.operator' then
if class then
class.operators[#class.operators+1] = doc
doc.class = class
end
bindCommentsToDoc(doc, comments)
comments = {}
elseif doc.type == 'doc.overload' then
if class then
class.calls[#class.calls+1] = doc
doc.class = class
end
elseif doc.type == 'doc.alias'
or doc.type == 'doc.enum' then
bindCommentsToDoc(doc, comments)
comments = {}
elseif doc.type == 'doc.comment' then
comments[#comments+1] = doc
elseif doc.type == 'doc.source' then
source = doc
goto CONTINUE
end
source = nil
::CONTINUE::
end
end
local function bindDocWithSources(sources, binded)
if not binded then
return
end
local lastDoc = binded[#binded]
if not lastDoc then
return
end
for _, doc in ipairs(binded) do
doc.bindGroup = binded
end
bindGeneric(binded)
bindCommentsAndFields(binded)
bindReturnIndex(binded)
local row = guide.rowColOf(lastDoc.finish)
local suc = bindDocsBetween(sources, binded, guide.positionOf(row, 0), lastDoc.start)
if not suc then
bindDocsBetween(sources, binded, guide.positionOf(row + 1, 0), guide.positionOf(row + 2, 0))
end
end
local bindDocAccept = {
'local' , 'setlocal' , 'setglobal',
'setfield' , 'setmethod' , 'setindex' ,
'tablefield', 'tableindex', 'self' ,
'function' , 'table' , '...' ,
}
local function bindDocs(state)
local text = state.lua
local sources = {}
guide.eachSourceTypes(state.ast, bindDocAccept, function (src)
sources[#sources+1] = src
end)
table.sort(sources, function (a, b)
return a.start < b.start
end)
local binded
for i, doc in ipairs(state.ast.docs) do
if not binded then
binded = {}
state.ast.docs.groups[#state.ast.docs.groups+1] = binded
end
binded[#binded+1] = doc
if isTailComment(text, doc) then
bindDocWithSources(sources, binded)
binded = nil
else
local nextDoc = state.ast.docs[i+1]
if not isNextLine(doc, nextDoc) then
bindDocWithSources(sources, binded)
binded = nil
end
if not isContinuedDoc(doc, nextDoc)
and not isTailComment(text, nextDoc) then
bindDocWithSources(sources, binded)
binded = nil
end
end
end
end
local function findTouch(state, doc)
local text = state.lua
local pos = guide.positionToOffset(state, doc.originalComment.start)
for i = pos - 2, 1, -1 do
local c = text:sub(i, i)
if c == '\r'
or c == '\n' then
break
elseif c ~= ' '
and c ~= '\t' then
doc.touch = guide.offsetToPosition(state, i)
break
end
end
end
return function (state)
local ast = state.ast
local comments = state.comms
table.sort(comments, function (a, b)
return a.start < b.start
end)
ast.docs = {
type = 'doc',
parent = ast,
groups = {},
}
pushWarning = function (err)
local errs = state.errs
if err.finish < err.start then
err.finish = err.start
end
local last = errs[#errs]
if last then
if last.start <= err.start and last.finish >= err.finish then
return
end
end
err.level = err.level or 'Warning'
errs[#errs+1] = err
return err
end
Lines = state.lines
local ci = 1
NextComment = function (offset, peek)
local comment = comments[ci + (offset or 0)]
if not peek then
ci = ci + 1 + (offset or 0)
end
return comment
end
local function insertDoc(doc, comment)
ast.docs[#ast.docs+1] = doc
doc.parent = ast.docs
if ast.start > doc.start then
ast.start = doc.start
end
if ast.finish < doc.finish then
ast.finish = doc.finish
end
doc.originalComment = comment
if comment.type == 'comment.long' then
findTouch(state, doc)
end
end
while true do
local comment = NextComment()
if not comment then
break
end
local doc, rests = buildLuaDoc(comment)
if doc then
insertDoc(doc, comment)
if rests then
for _, rest in ipairs(rests) do
insertDoc(rest, comment)
end
end
end
end
ast.docs.start = ast.start
ast.docs.finish = ast.finish
if #ast.docs == 0 then
return
end
bindDocs(state)
end