diff options
author | chai <chaifix@163.com> | 2021-10-30 11:42:13 +0800 |
---|---|---|
committer | chai <chaifix@163.com> | 2021-10-30 11:42:13 +0800 |
commit | 53364ddc2e09362cb17432abf4fb598557554a9f (patch) | |
tree | 8d2deafc82aceb13db31938a2aecc70927fc1457 /Data/Libraries/LDoc/ldoc/parse.lua | |
parent | 42ec7286b2d36a9ba22925f816a17cb1cc2aa5ce (diff) |
+ LDoc
Diffstat (limited to 'Data/Libraries/LDoc/ldoc/parse.lua')
-rw-r--r-- | Data/Libraries/LDoc/ldoc/parse.lua | 430 |
1 files changed, 430 insertions, 0 deletions
diff --git a/Data/Libraries/LDoc/ldoc/parse.lua b/Data/Libraries/LDoc/ldoc/parse.lua new file mode 100644 index 0000000..11ade60 --- /dev/null +++ b/Data/Libraries/LDoc/ldoc/parse.lua @@ -0,0 +1,430 @@ +-- parsing code for doc comments + +local utils = require 'pl.utils' +local List = require 'pl.List' +-- local Map = require 'pl.Map' +local stringio = require 'pl.stringio' +local lexer = require 'ldoc.lexer' +local tools = require 'ldoc.tools' +local doc = require 'ldoc.doc' +local Item,File = doc.Item,doc.File +local unpack = utils.unpack + +------ Parsing the Source -------------- +-- This uses the lexer from PL, but it should be possible to use Peter Odding's +-- excellent Lpeg based lexer instead. + +local parse = {} + +local tnext, append = lexer.skipws, table.insert + +-- a pattern particular to LuaDoc tag lines: the line must begin with @TAG, +-- followed by the value, which may extend over several lines. +local luadoc_tag = '^%s*@(%w+)' +local luadoc_tag_value = luadoc_tag..'(.*)' +local luadoc_tag_mod_and_value = luadoc_tag..'%[([^%]]*)%](.*)' + +-- assumes that the doc comment consists of distinct tag lines +local function parse_at_tags(text) + local lines = stringio.lines(text) + local preamble, line = tools.grab_while_not(lines,luadoc_tag) + local tag_items = {} + local follows + while line do + local tag, mod_string, rest = line :match(luadoc_tag_mod_and_value) + if not tag then tag, rest = line :match (luadoc_tag_value) end + local modifiers + if mod_string then + modifiers = { } + for x in mod_string :gmatch "[^,]+" do + local k, v = x :match "^([^=]+)=(.*)$" + if not k then k, v = x, true end -- wuz x, x + modifiers[k] = v + end + end + -- follows: end of current tag + -- line: beginning of next tag (for next iteration) + follows, line = tools.grab_while_not(lines,luadoc_tag) + append(tag_items,{tag, rest .. '\n' .. follows, modifiers}) + end + return preamble,tag_items +end + +--local colon_tag = '%s*(%a+):%s' +local colon_tag = '%s*(%S-):%s' +local colon_tag_value = colon_tag..'(.*)' + +local function parse_colon_tags (text) + local lines = stringio.lines(text) + local preamble, line = tools.grab_while_not(lines,colon_tag) + local tag_items, follows = {} + while line do + local tag, rest = line:match(colon_tag_value) + follows, line = tools.grab_while_not(lines,colon_tag) + local value = rest .. '\n' .. follows + if tag:match '^[%?!]' then + tag = tag:gsub('^!','') + value = tag .. ' ' .. value + tag = 'tparam' + end + append(tag_items,{tag, value}) + end + return preamble,tag_items +end + +-- Tags are stored as an ordered multi map from strings to strings +-- If the same key is used, then the value becomes a list +local Tags = {} +Tags.__index = Tags + +function Tags.new (t,name) + local class + if name then + class = t + t = {} + end + t._order = List() + local tags = setmetatable(t,Tags) + if name then + tags:add('class',class) + tags:add('name',name) + end + return tags +end + +function Tags:add (tag,value,modifiers) + if modifiers then -- how modifiers are encoded + value = {value,modifiers=modifiers} + end + local ovalue = self:get(tag) + if ovalue then + ovalue:append(value) + value = ovalue + end + rawset(self,tag,value) + if not ovalue then + self._order:append(tag) + end +end + +function Tags:get (tag) + local ovalue = rawget(self,tag) + if ovalue then -- previous value? + if getmetatable(ovalue) ~= List then + ovalue = List{ovalue} + end + return ovalue + end +end + +function Tags:iter () + return self._order:iter() +end + +local function comment_contains_tags (comment,args) + return (args.colon and comment:find ': ') or (not args.colon and comment:find '@') +end + +-- This takes the collected comment block, and uses the docstyle to +-- extract tags and values. Assume that the summary ends in a period or a question +-- mark, and everything else in the preamble is the description. +-- If a tag appears more than once, then its value becomes a list of strings. +-- Alias substitution and @TYPE NAME shortcutting is handled by Item.check_tag +local function extract_tags (s,args) + local preamble,tag_items + if s:match '^%s*$' then return {} end + if args.colon then --and s:match ':%s' and not s:match '@%a' then + preamble,tag_items = parse_colon_tags(s) + else + preamble,tag_items = parse_at_tags(s) + end + local strip = tools.strip + local summary, description = preamble:match('^(.-[%.?])(%s.+)') + if not summary then + -- perhaps the first sentence did not have a . or ? terminating it. + -- Then try split at linefeed + summary, description = preamble:match('^(.-\n\n)(.+)') + if not summary then + summary = preamble + end + end -- and strip(description) ? + local tags = Tags.new{summary=summary and strip(summary) or '',description=description or ''} + for _,item in ipairs(tag_items) do + local tag, value, modifiers = Item.check_tag(tags,unpack(item)) + -- treat multiline values more gently.. + if not value:match '\n[^\n]+\n' then + value = strip(value) + end + + tags:add(tag,value,modifiers) + end + return tags --Map(tags) +end + + +-- parses a Lua or C file, looking for ldoc comments. These are like LuaDoc comments; +-- they start with multiple '-'. (Block commments are allowed) +-- If they don't define a name tag, then by default +-- it is assumed that a function definition follows. If it is the first comment +-- encountered, then ldoc looks for a call to module() to find the name of the +-- module if there isn't an explicit module name specified. + +local function parse_file(fname, lang, package, args) + local F = File(fname) + local module_found, first_comment = false,true + local current_item, module_item + + F.args = args + F.lang = lang + F.base = package + + local tok,f = lang.lexer(fname) + if not tok then return nil end + + local function lineno () + return tok:lineno() + end + + function F:warning (msg,kind,line) + line = line or lineno() + Item.had_warning = true + io.stderr:write(fname..':'..line..': '..msg,'\n') + end + + function F:error (msg) + self:warning(msg,'error') + io.stderr:write('LDoc error\n') + os.exit(1) + end + + local function add_module(tags,module_found,old_style) + tags:add('name',module_found) + tags:add('class','module') + local item = F:new_item(tags,lineno()) + item.old_style = old_style + module_item = item + end + + local mod + local t,v = tnext(tok) + -- with some coding styles first comment is standard boilerplate; option to ignore this. + if args.boilerplate and t == 'comment' then + -- hack to deal with boilerplate inside Lua block comments + if v:match '%s*%-%-%[%[' then lang:grab_block_comment(v,tok) end + t,v = tnext(tok) + end + if t == '#' then -- skip Lua shebang line, if present + while t and t ~= 'comment' do t,v = tnext(tok) end + if t == nil then + F:warning('empty file') + return nil + end + end + if lang.parse_module_call and t ~= 'comment' then + local prev_token + while t do + if prev_token ~= '.' and prev_token ~= ':' and t == 'iden' and v == 'module' then + break + end + prev_token = t + t, v = tnext(tok) + end + if not t then + if not args.ignore then + F:warning("no module() call found; no initial doc comment") + end + --return nil + else + mod,t,v = lang:parse_module_call(tok,t,v) + if mod and mod ~= '...' then + add_module(Tags.new{summary='(no description)'},mod,true) + first_comment = false + module_found = true + end + end + end + local ok, err = xpcall(function() + while t do + if t == 'comment' then + local comment = {} + local ldoc_comment,block = lang:start_comment(v) + + if ldoc_comment and block then + t,v = lang:grab_block_comment(v,tok) + end + + if lang:empty_comment(v) then -- ignore rest of empty start comments + t,v = tok() + if t == 'space' and not v:match '\n' then + t,v = tok() + end + end + + while t and t == 'comment' do + v = lang:trim_comment(v) + append(comment,v) + t,v = tok() + if t == 'space' and not v:match '\n' then + t,v = tok() + end + end + + if t == 'space' then t,v = tnext(tok) end + + local item_follows, tags, is_local, case, parse_error + if ldoc_comment then + comment = table.concat(comment) + if comment:match '^%s*$' then + ldoc_comment = nil + end + end + if ldoc_comment then + if first_comment then + first_comment = false + else + item_follows, is_local, case = lang:item_follows(t,v,tok) + if not item_follows then + parse_error = is_local + is_local = false + end + end + + if item_follows or comment_contains_tags(comment,args) then + tags = extract_tags(comment,args) + + -- explicitly named @module (which is recommended) + if doc.project_level(tags.class) then + module_found = tags.name + -- might be a module returning a single function! + if tags.param or tags['return'] then + local parms, ret = tags.param, tags['return'] + local name = tags.name + tags.param = nil + tags['return'] = nil + tags['class'] = nil + tags['name'] = nil + add_module(tags,name,false) + tags = { + summary = '', + name = 'returns...', + class = 'function', + ['return'] = ret, + param = parms + } + end + end + doc.expand_annotation_item(tags,current_item) + -- if the item has an explicit name or defined meaning + -- then don't continue to do any code analysis! + -- Watch out for the case where there are field or param tags + -- but no class, since these will be fixed up later as module/class + -- entities + if (tags.field or tags.param) and not tags.class then + parse_error = false + end + if tags.name then + if not tags.class then + F:warning("no type specified, assuming function: '"..tags.name.."'") + tags:add('class','function') + end + item_follows, is_local, parse_error = false, false, false + elseif args.no_args_infer then + F:error("No name and type provided (no_args_infer)") + elseif lang:is_module_modifier (tags) then + if not item_follows then + F:warning("@usage or @export followed by unknown code") + break + end + item_follows(tags,tok) + local res, value, tagname = lang:parse_module_modifier(tags,tok,F) + if not res then F:warning(value); break + else + if tagname then + module_item:set_tag(tagname,value) + end + -- don't continue to make an item! + ldoc_comment = false + end + end + end + if parse_error then + F:warning('definition cannot be parsed - '..parse_error) + end + end + -- some hackery necessary to find the module() call + if not module_found and ldoc_comment then + local old_style + module_found,t,v = lang:find_module(tok,t,v) + -- right, we can add the module object ... + old_style = module_found ~= nil + if not module_found or module_found == '...' then + -- we have to guess the module name + module_found = tools.this_module_name(package,fname) + end + if not tags then tags = extract_tags(comment,args) end + add_module(tags,module_found,old_style) + tags = nil + if not t then + F:warning('contains no items','warning',1) + break; + end -- run out of file! + -- if we did bump into a doc comment, then we can continue parsing it + end + + -- end of a block of document comments + if ldoc_comment and tags then + local line = lineno() + if t ~= nil then + if item_follows then -- parse the item definition + local err = item_follows(tags,tok) + if err then F:error(err) end + elseif parse_error then + F:warning('definition cannot be parsed - '..parse_error) + else + lang:parse_extra(tags,tok,case) + end + end + if is_local or tags['local'] then + tags:add('local',true) + end + -- support for standalone fields/properties of classes/modules + if (tags.field or tags.param) and not tags.class then + -- the hack is to take a subfield and pull out its name, + -- (see Tag:add above) but let the subfield itself go through + -- with any modifiers. + local fp = tags.field or tags.param + if type(fp) == 'table' then fp = fp[1] end + fp = tools.extract_identifier(fp) + tags:add('name',fp) + tags:add('class','field') + end + if tags.name then + current_item = F:new_item(tags,line) + current_item.inferred = item_follows ~= nil + if doc.project_level(tags.class) then + if module_item then + F:error("Module already declared!") + end + module_item = current_item + end + end + if not t then break end + end + end + if t ~= 'comment' then t,v = tok() end + end + end,debug.traceback) + if not ok then return F, err end + if f then f:close() end + return F +end + +function parse.file(name,lang, args) + local F,err = parse_file(name,lang,args.package,args) + if err or not F then return F,err end + local ok,err = xpcall(function() F:finish() end,debug.traceback) + if not ok then return F,err end + return F +end + +return parse |