.dotfiles/nvim/mason/packages/lua-language-server/libexec/script/parser/luadoc.lua

2067 lines
57 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
local lockResume = false
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
if not lockResume then
lockResume = true
while pushResume() do end
lockResume = false
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 ()
local requireName = parseName('doc.meta.name')
return {
type = 'doc.meta',
name = requireName,
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.name = 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 = util.trim(text:sub(2))
end
if text:sub(1, 1) == '#' then
comment = util.trim(text:sub(2))
end
if text:sub(1, 2) == '--' then
comment = util.trim(text:sub(3))
end
if comment:find '^%s*[\'"[]'
and comment:find '[\'"%]]%s*$' then
local state = compile(comment:gsub('^%s+', ''), 'String')
if state and state.ast then
comment = state.ast[1]
end
end
return util.trim(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 = math.max(comment.finish, result.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
lockResume = false
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