PageRenderTime 57ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/ldoc/parse.lua

http://github.com/stevedonovan/LDoc
Lua | 440 lines | 355 code | 39 blank | 46 comment | 88 complexity | a0e0a810442521b898583259ff0a8e93 MD5 | raw file
  1. -- parsing code for doc comments
  2. local utils = require 'pl.utils'
  3. local List = require 'pl.List'
  4. local Map = require 'pl.Map'
  5. local stringio = require 'pl.stringio'
  6. local lexer = require 'ldoc.lexer'
  7. local tools = require 'ldoc.tools'
  8. local doc = require 'ldoc.doc'
  9. local Item,File = doc.Item,doc.File
  10. local unpack = utils.unpack
  11. ------ Parsing the Source --------------
  12. -- This uses the lexer from PL, but it should be possible to use Peter Odding's
  13. -- excellent Lpeg based lexer instead.
  14. local parse = {}
  15. local tnext, append = lexer.skipws, table.insert
  16. -- a pattern particular to LuaDoc tag lines: the line must begin with @TAG,
  17. -- followed by the value, which may extend over several lines.
  18. local luadoc_tag = '^%s*@(%w+)'
  19. local luadoc_tag_value = luadoc_tag..'(.*)'
  20. local luadoc_tag_mod_and_value = luadoc_tag..'%[([^%]]*)%](.*)'
  21. -- assumes that the doc comment consists of distinct tag lines
  22. local function parse_at_tags(text)
  23. local lines = stringio.lines(text)
  24. local preamble, line = tools.grab_while_not(lines,luadoc_tag)
  25. local tag_items = {}
  26. local follows
  27. while line do
  28. local tag, mod_string, rest = line :match(luadoc_tag_mod_and_value)
  29. if not tag then tag, rest = line :match (luadoc_tag_value) end
  30. local modifiers
  31. if mod_string then
  32. modifiers = { }
  33. for x in mod_string :gmatch "[^,]+" do
  34. local k, v = x :match "^([^=]+)=(.*)$"
  35. if not k then k, v = x, true end -- wuz x, x
  36. modifiers[k] = v
  37. end
  38. end
  39. -- follows: end of current tag
  40. -- line: beginning of next tag (for next iteration)
  41. follows, line = tools.grab_while_not(lines,luadoc_tag)
  42. append(tag_items,{tag, rest .. '\n' .. follows, modifiers})
  43. end
  44. return preamble,tag_items
  45. end
  46. --local colon_tag = '%s*(%a+):%s'
  47. local colon_tag = '%s*(%S-):%s'
  48. local colon_tag_value = colon_tag..'(.*)'
  49. local function parse_colon_tags (text)
  50. local lines = stringio.lines(text)
  51. local preamble, line = tools.grab_while_not(lines,colon_tag)
  52. local tag_items, follows = {}
  53. while line do
  54. local tag, rest = line:match(colon_tag_value)
  55. follows, line = tools.grab_while_not(lines,colon_tag)
  56. local value = rest .. '\n' .. follows
  57. if tag:match '^[%?!]' then
  58. tag = tag:gsub('^!','')
  59. value = tag .. ' ' .. value
  60. tag = 'tparam'
  61. end
  62. append(tag_items,{tag, value})
  63. end
  64. return preamble,tag_items
  65. end
  66. -- Tags are stored as an ordered multi map from strings to strings
  67. -- If the same key is used, then the value becomes a list
  68. local Tags = {}
  69. Tags.__index = Tags
  70. function Tags.new (t,name)
  71. local class
  72. if name then
  73. class = t
  74. t = {}
  75. end
  76. t._order = List()
  77. local tags = setmetatable(t,Tags)
  78. if name then
  79. tags:add('class',class)
  80. tags:add('name',name)
  81. end
  82. return tags
  83. end
  84. function Tags:add (tag,value,modifiers)
  85. if modifiers then -- how modifiers are encoded
  86. value = {value,modifiers=modifiers}
  87. end
  88. local ovalue = self:get(tag)
  89. if ovalue then
  90. ovalue:append(value)
  91. value = ovalue
  92. end
  93. rawset(self,tag,value)
  94. if not ovalue then
  95. self._order:append(tag)
  96. end
  97. end
  98. function Tags:get (tag)
  99. local ovalue = rawget(self,tag)
  100. if ovalue then -- previous value?
  101. if getmetatable(ovalue) ~= List then
  102. ovalue = List{ovalue}
  103. end
  104. return ovalue
  105. end
  106. end
  107. function Tags:iter ()
  108. return self._order:iter()
  109. end
  110. local function comment_contains_tags (comment,args)
  111. return (args.colon and comment:find ': ') or (not args.colon and comment:find '@')
  112. end
  113. -- This takes the collected comment block, and uses the docstyle to
  114. -- extract tags and values. Assume that the summary ends in a period or a question
  115. -- mark, and everything else in the preamble is the description.
  116. -- If a tag appears more than once, then its value becomes a list of strings.
  117. -- Alias substitution and @TYPE NAME shortcutting is handled by Item.check_tag
  118. local function extract_tags (s,args)
  119. local preamble,tag_items
  120. if s:match '^%s*$' then return {} end
  121. if args.colon then --and s:match ':%s' and not s:match '@%a' then
  122. preamble,tag_items = parse_colon_tags(s)
  123. else
  124. preamble,tag_items = parse_at_tags(s)
  125. end
  126. local strip = tools.strip
  127. local summary, description = preamble:match('^(.-[%.?])(%s.+)')
  128. if not summary then
  129. -- perhaps the first sentence did not have a . or ? terminating it.
  130. -- Then try split at linefeed
  131. summary, description = preamble:match('^(.-\n\n)(.+)')
  132. if not summary then
  133. summary = preamble
  134. end
  135. end -- and strip(description) ?
  136. local tags = Tags.new{summary=summary and strip(summary) or '',description=description or ''}
  137. for _,item in ipairs(tag_items) do
  138. local tag, value, modifiers = Item.check_tag(tags,unpack(item))
  139. -- treat multiline values more gently..
  140. if not value:match '\n[^\n]+\n' then
  141. value = strip(value)
  142. end
  143. tags:add(tag,value,modifiers)
  144. end
  145. return tags --Map(tags)
  146. end
  147. local _xpcall = xpcall
  148. if true then
  149. _xpcall = function(f) return true, f() end
  150. end
  151. -- parses a Lua or C file, looking for ldoc comments. These are like LuaDoc comments;
  152. -- they start with multiple '-'. (Block commments are allowed)
  153. -- If they don't define a name tag, then by default
  154. -- it is assumed that a function definition follows. If it is the first comment
  155. -- encountered, then ldoc looks for a call to module() to find the name of the
  156. -- module if there isn't an explicit module name specified.
  157. local function parse_file(fname, lang, package, args)
  158. local line,f = 1
  159. local F = File(fname)
  160. local module_found, first_comment = false,true
  161. local current_item, module_item
  162. F.args = args
  163. F.lang = lang
  164. F.base = package
  165. local tok,f = lang.lexer(fname)
  166. if not tok then return nil end
  167. local function lineno ()
  168. return tok:lineno()
  169. end
  170. local function filename () return fname end
  171. function F:warning (msg,kind,line)
  172. kind = kind or 'warning'
  173. line = line or lineno()
  174. Item.had_warning = true
  175. io.stderr:write(fname..':'..line..': '..msg,'\n')
  176. end
  177. function F:error (msg)
  178. self:warning(msg,'error')
  179. io.stderr:write('LDoc error\n')
  180. os.exit(1)
  181. end
  182. local function add_module(tags,module_found,old_style)
  183. tags:add('name',module_found)
  184. tags:add('class','module')
  185. local item = F:new_item(tags,lineno())
  186. item.old_style = old_style
  187. module_item = item
  188. end
  189. local mod
  190. local t,v = tnext(tok)
  191. -- with some coding styles first comment is standard boilerplate; option to ignore this.
  192. if args.boilerplate and t == 'comment' then
  193. -- hack to deal with boilerplate inside Lua block comments
  194. if v:match '%s*%-%-%[%[' then lang:grab_block_comment(v,tok) end
  195. t,v = tnext(tok)
  196. end
  197. if t == '#' then -- skip Lua shebang line, if present
  198. while t and t ~= 'comment' do t,v = tnext(tok) end
  199. if t == nil then
  200. F:warning('empty file')
  201. return nil
  202. end
  203. end
  204. if lang.parse_module_call and t ~= 'comment' then
  205. local prev_token
  206. while t do
  207. if prev_token ~= '.' and prev_token ~= ':' and t == 'iden' and v == 'module' then
  208. break
  209. end
  210. prev_token = t
  211. t, v = tnext(tok)
  212. end
  213. if not t then
  214. if not args.ignore then
  215. F:warning("no module() call found; no initial doc comment")
  216. end
  217. --return nil
  218. else
  219. mod,t,v = lang:parse_module_call(tok,t,v)
  220. if mod and mod ~= '...' then
  221. add_module(Tags.new{summary='(no description)'},mod,true)
  222. first_comment = false
  223. module_found = true
  224. end
  225. end
  226. end
  227. local ok, err = xpcall(function()
  228. while t do
  229. if t == 'comment' then
  230. local comment = {}
  231. local ldoc_comment,block = lang:start_comment(v)
  232. if ldoc_comment and block then
  233. t,v = lang:grab_block_comment(v,tok)
  234. end
  235. if lang:empty_comment(v) then -- ignore rest of empty start comments
  236. t,v = tok()
  237. if t == 'space' and not v:match '\n' then
  238. t,v = tok()
  239. end
  240. end
  241. while t and t == 'comment' do
  242. v = lang:trim_comment(v)
  243. append(comment,v)
  244. t,v = tok()
  245. if t == 'space' and not v:match '\n' then
  246. t,v = tok()
  247. end
  248. end
  249. if t == 'space' then t,v = tnext(tok) end
  250. local item_follows, tags, is_local, case, parse_error
  251. if ldoc_comment then
  252. comment = table.concat(comment)
  253. if comment:match '^%s*$' then
  254. ldoc_comment = nil
  255. end
  256. end
  257. if ldoc_comment then
  258. if first_comment then
  259. first_comment = false
  260. else
  261. item_follows, is_local, case = lang:item_follows(t,v,tok)
  262. if not item_follows then
  263. parse_error = is_local
  264. is_local = false
  265. end
  266. end
  267. if item_follows or comment_contains_tags(comment,args) then
  268. tags = extract_tags(comment,args)
  269. -- explicitly named @module (which is recommended)
  270. if doc.project_level(tags.class) then
  271. module_found = tags.name
  272. -- might be a module returning a single function!
  273. if tags.param or tags['return'] then
  274. local parms, ret, summ = tags.param, tags['return'],tags.summary
  275. local name = tags.name
  276. tags.param = nil
  277. tags['return'] = nil
  278. tags['class'] = nil
  279. tags['name'] = nil
  280. add_module(tags,name,false)
  281. tags = {
  282. summary = '',
  283. name = 'returns...',
  284. class = 'function',
  285. ['return'] = ret,
  286. param = parms
  287. }
  288. end
  289. end
  290. doc.expand_annotation_item(tags,current_item)
  291. -- if the item has an explicit name or defined meaning
  292. -- then don't continue to do any code analysis!
  293. -- Watch out for the case where there are field or param tags
  294. -- but no class, since these will be fixed up later as module/class
  295. -- entities
  296. if (tags.field or tags.param) and not tags.class then
  297. parse_error = false
  298. end
  299. if tags.name then
  300. if not tags.class then
  301. F:warning("no type specified, assuming function: '"..tags.name.."'")
  302. tags:add('class','function')
  303. end
  304. item_follows, is_local, parse_error = false, false, false
  305. elseif args.no_args_infer then
  306. F:error("No name and type provided (no_args_infer)")
  307. elseif lang:is_module_modifier (tags) then
  308. if not item_follows then
  309. F:warning("@usage or @export followed by unknown code")
  310. break
  311. end
  312. item_follows(tags,tok)
  313. local res, value, tagname = lang:parse_module_modifier(tags,tok,F)
  314. if not res then F:warning(value); break
  315. else
  316. if tagname then
  317. module_item:set_tag(tagname,value)
  318. end
  319. -- don't continue to make an item!
  320. ldoc_comment = false
  321. end
  322. end
  323. end
  324. if parse_error then
  325. F:warning('definition cannot be parsed - '..parse_error)
  326. end
  327. end
  328. -- some hackery necessary to find the module() call
  329. if not module_found and ldoc_comment then
  330. local old_style
  331. module_found,t,v = lang:find_module(tok,t,v)
  332. -- right, we can add the module object ...
  333. old_style = module_found ~= nil
  334. if not module_found or module_found == '...' then
  335. -- we have to guess the module name
  336. module_found = tools.this_module_name(package,fname)
  337. end
  338. if not tags then tags = extract_tags(comment,args) end
  339. add_module(tags,module_found,old_style)
  340. tags = nil
  341. if not t then
  342. F:warning('contains no items','warning',1)
  343. break;
  344. end -- run out of file!
  345. -- if we did bump into a doc comment, then we can continue parsing it
  346. end
  347. -- end of a block of document comments
  348. if ldoc_comment and tags then
  349. local line = lineno()
  350. if t ~= nil then
  351. if item_follows then -- parse the item definition
  352. local err = item_follows(tags,tok)
  353. if err then F:error(err) end
  354. elseif parse_error then
  355. F:warning('definition cannot be parsed - '..parse_error)
  356. else
  357. lang:parse_extra(tags,tok,case)
  358. end
  359. end
  360. if is_local or tags['local'] then
  361. tags:add('local',true)
  362. end
  363. -- support for standalone fields/properties of classes/modules
  364. if (tags.field or tags.param) and not tags.class then
  365. -- the hack is to take a subfield and pull out its name,
  366. -- (see Tag:add above) but let the subfield itself go through
  367. -- with any modifiers.
  368. local fp = tags.field or tags.param
  369. if type(fp) == 'table' then fp = fp[1] end
  370. fp = tools.extract_identifier(fp)
  371. tags:add('name',fp)
  372. tags:add('class','field')
  373. end
  374. if tags.name then
  375. current_item = F:new_item(tags,line)
  376. current_item.inferred = item_follows ~= nil
  377. if doc.project_level(tags.class) then
  378. if module_item then
  379. F:error("Module already declared!")
  380. end
  381. module_item = current_item
  382. end
  383. end
  384. if not t then break end
  385. end
  386. end
  387. if t ~= 'comment' then t,v = tok() end
  388. end
  389. end,debug.traceback)
  390. if not ok then return F, err end
  391. if f then f:close() end
  392. return F
  393. end
  394. function parse.file(name,lang, args)
  395. local F,err = parse_file(name,lang,args.package,args)
  396. if err or not F then return F,err end
  397. local ok,err = xpcall(function() F:finish() end,debug.traceback)
  398. if not ok then return F,err end
  399. return F
  400. end
  401. return parse