lua2dox.lua 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. --[[--------------------------------------------------------------------------
  2. -- Copyright (C) 2012 by Simon Dales --
  3. -- simon@purrsoft.co.uk --
  4. -- --
  5. -- This program is free software; you can redistribute it and/or modify --
  6. -- it under the terms of the GNU General Public License as published by --
  7. -- the Free Software Foundation; either version 2 of the License, or --
  8. -- (at your option) any later version. --
  9. -- --
  10. -- This program is distributed in the hope that it will be useful, --
  11. -- but WITHOUT ANY WARRANTY; without even the implied warranty of --
  12. -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --
  13. -- GNU General Public License for more details. --
  14. -- --
  15. -- You should have received a copy of the GNU General Public License --
  16. -- along with this program; if not, write to the --
  17. -- Free Software Foundation, Inc., --
  18. -- 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. --
  19. ----------------------------------------------------------------------------]]
  20. --[[!
  21. Lua-to-Doxygen converter
  22. Partially from lua2dox
  23. http://search.cpan.org/~alec/Doxygen-Lua-0.02/lib/Doxygen/Lua.pm
  24. RUNNING
  25. -------
  26. This script "lua2dox.lua" gets called by "gen_vimdoc.py".
  27. DEBUGGING/DEVELOPING
  28. ---------------------
  29. 1. To debug, run gen_vimdoc.py with --keep-tmpfiles:
  30. python3 scripts/gen_vimdoc.py -t treesitter --keep-tmpfiles
  31. 2. The filtered result will be written to ./tmp-lua2dox-doc/….lua.c
  32. Doxygen must be on your system. You can experiment like so:
  33. - Run "doxygen -g" to create a default Doxyfile.
  34. - Then alter it to let it recognise lua. Add the following line:
  35. FILE_PATTERNS = *.lua
  36. - Then run "doxygen".
  37. The core function reads the input file (filename or stdin) and outputs some pseudo C-ish language.
  38. It only has to be good enough for doxygen to see it as legal.
  39. One limitation is that each line is treated separately (except for long comments).
  40. The implication is that class and function declarations must be on the same line.
  41. Some functions can have their parameter lists extended over multiple lines to make it look neat.
  42. Managing this where there are also some comments is a bit more coding than I want to do at this stage,
  43. so it will probably not document accurately if we do do this.
  44. However I have put in a hack that will insert the "missing" close paren.
  45. The effect is that you will get the function documented, but not with the parameter list you might expect.
  46. ]]
  47. local _debug_outfile = nil
  48. local _debug_output = {}
  49. local function class()
  50. local newClass = {} -- a new class newClass
  51. -- the class will be the metatable for all its newInstanceects,
  52. -- and they will look up their methods in it.
  53. newClass.__index = newClass
  54. -- expose a constructor which can be called by <classname>(<args>)
  55. setmetatable(newClass, {
  56. __call = function(class_tbl, ...)
  57. local newInstance = {}
  58. setmetatable(newInstance, newClass)
  59. --if init then
  60. -- init(newInstance,...)
  61. if class_tbl.init then
  62. class_tbl.init(newInstance, ...)
  63. end
  64. return newInstance
  65. end
  66. })
  67. return newClass
  68. end
  69. -- write to stdout
  70. local function TCore_IO_write(Str)
  71. if Str then
  72. io.write(Str)
  73. if _debug_outfile then
  74. table.insert(_debug_output, Str)
  75. end
  76. end
  77. end
  78. -- write to stdout
  79. local function TCore_IO_writeln(Str)
  80. TCore_IO_write(Str)
  81. TCore_IO_write('\n')
  82. end
  83. -- trims a string
  84. local function string_trim(Str)
  85. return Str:match('^%s*(.-)%s*$')
  86. end
  87. -- split a string
  88. --!
  89. --! \param Str
  90. --! \param Pattern
  91. --! \returns table of string fragments
  92. ---@return string[]
  93. local function string_split(Str, Pattern)
  94. local splitStr = {}
  95. local fpat = '(.-)' .. Pattern
  96. local last_end = 1
  97. local str, e, cap = string.find(Str, fpat, 1)
  98. while str do
  99. if str ~= 1 or cap ~= '' then
  100. table.insert(splitStr, cap)
  101. end
  102. last_end = e + 1
  103. str, e, cap = string.find(Str, fpat, last_end)
  104. end
  105. if last_end <= #Str then
  106. cap = string.sub(Str, last_end)
  107. table.insert(splitStr, cap)
  108. end
  109. return splitStr
  110. end
  111. -------------------------------
  112. -- file buffer
  113. --!
  114. --! an input file buffer
  115. local TStream_Read = class()
  116. -- get contents of file
  117. --!
  118. --! \param Filename name of file to read (or nil == stdin)
  119. function TStream_Read.getContents(this, Filename)
  120. assert(Filename, ('invalid file: %s'):format(Filename))
  121. -- get lines from file
  122. -- syphon lines to our table
  123. local filecontents = {}
  124. for line in io.lines(Filename) do
  125. table.insert(filecontents, line)
  126. end
  127. if filecontents then
  128. this.filecontents = filecontents
  129. this.contentsLen = #filecontents
  130. this.currentLineNo = 1
  131. end
  132. return filecontents
  133. end
  134. -- get lineno
  135. function TStream_Read.getLineNo(this)
  136. return this.currentLineNo
  137. end
  138. -- get a line
  139. function TStream_Read.getLine(this)
  140. local line
  141. if this.currentLine then
  142. line = this.currentLine
  143. this.currentLine = nil
  144. else
  145. -- get line
  146. if this.currentLineNo <= this.contentsLen then
  147. line = this.filecontents[this.currentLineNo]
  148. this.currentLineNo = this.currentLineNo + 1
  149. else
  150. line = ''
  151. end
  152. end
  153. return line
  154. end
  155. -- save line fragment
  156. function TStream_Read.ungetLine(this, LineFrag)
  157. this.currentLine = LineFrag
  158. end
  159. -- is it eof?
  160. function TStream_Read.eof(this)
  161. if this.currentLine or this.currentLineNo <= this.contentsLen then
  162. return false
  163. end
  164. return true
  165. end
  166. -- output stream
  167. local TStream_Write = class()
  168. -- constructor
  169. function TStream_Write.init(this)
  170. this.tailLine = {}
  171. end
  172. -- write immediately
  173. function TStream_Write.write(_, Str)
  174. TCore_IO_write(Str)
  175. end
  176. -- write immediately
  177. function TStream_Write.writeln(_, Str)
  178. TCore_IO_writeln(Str)
  179. end
  180. -- write immediately
  181. function TStream_Write.writelnComment(_, Str)
  182. TCore_IO_write('// ZZ: ')
  183. TCore_IO_writeln(Str)
  184. end
  185. -- write to tail
  186. function TStream_Write.writelnTail(this, Line)
  187. if not Line then
  188. Line = ''
  189. end
  190. table.insert(this.tailLine, Line)
  191. end
  192. -- output tail lines
  193. function TStream_Write.write_tailLines(this)
  194. for _, line in ipairs(this.tailLine) do
  195. TCore_IO_writeln(line)
  196. end
  197. TCore_IO_write('// Lua2DoX new eof')
  198. end
  199. -- input filter
  200. local TLua2DoX_filter = class()
  201. -- allow us to do errormessages
  202. function TLua2DoX_filter.warning(this, Line, LineNo, Legend)
  203. this.outStream:writelnTail(
  204. '//! \todo warning! ' .. Legend .. ' (@' .. LineNo .. ')"' .. Line .. '"'
  205. )
  206. end
  207. -- trim comment off end of string
  208. --!
  209. --! If the string has a comment on the end, this trims it off.
  210. --!
  211. local function TString_removeCommentFromLine(Line)
  212. local pos_comment = string.find(Line, '%-%-')
  213. local tailComment
  214. if pos_comment then
  215. Line = string.sub(Line, 1, pos_comment - 1)
  216. tailComment = string.sub(Line, pos_comment)
  217. end
  218. return Line, tailComment
  219. end
  220. -- get directive from magic
  221. local function getMagicDirective(Line)
  222. local macro, tail
  223. local macroStr = '[\\@]'
  224. local pos_macro = string.find(Line, macroStr)
  225. if pos_macro then
  226. --! ....\\ macro...stuff
  227. --! ....\@ macro...stuff
  228. local line = string.sub(Line, pos_macro + 1)
  229. local space = string.find(line, '%s+')
  230. if space then
  231. macro = string.sub(line, 1, space - 1)
  232. tail = string_trim(string.sub(line, space + 1))
  233. else
  234. macro = line
  235. tail = ''
  236. end
  237. end
  238. return macro, tail
  239. end
  240. -- check comment for fn
  241. local function checkComment4fn(Fn_magic, MagicLines)
  242. local fn_magic = Fn_magic
  243. -- TCore_IO_writeln('// checkComment4fn "' .. MagicLines .. '"')
  244. local magicLines = string_split(MagicLines, '\n')
  245. local macro, tail
  246. for _, line in ipairs(magicLines) do
  247. macro, tail = getMagicDirective(line)
  248. if macro == 'fn' then
  249. fn_magic = tail
  250. -- TCore_IO_writeln('// found fn "' .. fn_magic .. '"')
  251. --else
  252. --TCore_IO_writeln('// not found fn "' .. line .. '"')
  253. end
  254. end
  255. return fn_magic
  256. end
  257. local types = { 'integer', 'number', 'string', 'table', 'list', 'boolean', 'function' }
  258. local tagged_types = { 'TSNode', 'LanguageTree' }
  259. -- Document these as 'table'
  260. local alias_types = { 'Range', 'Range4', 'Range6', 'TSMetadata' }
  261. -- Processes the file and writes filtered output to stdout.
  262. function TLua2DoX_filter.filter(this, AppStamp, Filename)
  263. local inStream = TStream_Read()
  264. local outStream = TStream_Write()
  265. this.outStream = outStream -- save to this obj
  266. if inStream:getContents(Filename) then
  267. -- output the file
  268. local line
  269. local fn_magic -- function name/def from magic comment
  270. outStream:writelnTail('// #######################')
  271. outStream:writelnTail('// app run:' .. AppStamp)
  272. outStream:writelnTail('// #######################')
  273. outStream:writelnTail()
  274. local state = '' -- luacheck: ignore 231 variable is set but never accessed.
  275. local offset = 0
  276. local generic = {}
  277. local l = 0
  278. while not (inStream:eof()) do
  279. line = string_trim(inStream:getLine())
  280. l = l + 1
  281. if string.sub(line, 1, 2) == '--' then -- it's a comment
  282. -- Allow people to write style similar to EmmyLua (since they are basically the same)
  283. -- instead of silently skipping things that start with ---
  284. if string.sub(line, 3, 3) == '@' then -- it's a magic comment
  285. offset = 0
  286. elseif string.sub(line, 1, 4) == '---@' then -- it's a magic comment
  287. offset = 1
  288. end
  289. line = line:gsub('@package', '@private')
  290. if vim.startswith(line, '---@cast')
  291. or vim.startswith(line, '---@diagnostic')
  292. or vim.startswith(line, '---@type') then
  293. -- Ignore LSP directives
  294. outStream:writeln('// gg:"' .. line .. '"')
  295. elseif string.sub(line, 3, 3) == '@' or string.sub(line, 1, 4) == '---@' then -- it's a magic comment
  296. state = 'in_magic_comment'
  297. local magic = string.sub(line, 4 + offset)
  298. local magic_split = string_split(magic, ' ')
  299. if magic_split[1] == 'param' then
  300. for _, type in ipairs(types) do
  301. magic = magic:gsub('^param%s+([a-zA-Z_?]+)%s+.*%((' .. type .. ')%)', 'param %1 %2')
  302. magic =
  303. magic:gsub('^param%s+([a-zA-Z_?]+)%s+.*%((' .. type .. '|nil)%)', 'param %1 %2')
  304. end
  305. magic_split = string_split(magic, ' ')
  306. elseif magic_split[1] == 'return' then
  307. for _, type in ipairs(types) do
  308. magic = magic:gsub('^return%s+.*%((' .. type .. ')%)', 'return %1')
  309. magic = magic:gsub('^return%s+.*%((' .. type .. '|nil)%)', 'return %1')
  310. end
  311. magic_split = string_split(magic, ' ')
  312. end
  313. if magic_split[1] == 'generic' then
  314. local generic_name, generic_type = line:match('@generic%s*(%w+)%s*:?%s*(.*)')
  315. if generic_type == '' then
  316. generic_type = 'any'
  317. end
  318. generic[generic_name] = generic_type
  319. else
  320. local type_index = 2
  321. if magic_split[1] == 'param' then
  322. type_index = type_index + 1
  323. end
  324. if magic_split[type_index] then
  325. -- fix optional parameters
  326. if magic_split[type_index] and magic_split[2]:find('%?$') then
  327. if not magic_split[type_index]:find('nil') then
  328. magic_split[type_index] = magic_split[type_index] .. '|nil'
  329. end
  330. magic_split[2] = magic_split[2]:sub(1, -2)
  331. end
  332. -- replace generic types
  333. if magic_split[type_index] then
  334. for k, v in pairs(generic) do
  335. magic_split[type_index] = magic_split[type_index]:gsub(k, v)
  336. end
  337. end
  338. for _, type in ipairs(tagged_types) do
  339. magic_split[type_index] =
  340. magic_split[type_index]:gsub(type, '|%1|')
  341. end
  342. for _, type in ipairs(alias_types) do
  343. magic_split[type_index] =
  344. magic_split[type_index]:gsub('^'..type..'$', 'table')
  345. end
  346. -- surround some types by ()
  347. for _, type in ipairs(types) do
  348. magic_split[type_index] =
  349. magic_split[type_index]:gsub('^(' .. type .. '|nil):?$', '(%1)')
  350. magic_split[type_index] =
  351. magic_split[type_index]:gsub('^(' .. type .. '):?$', '(%1)')
  352. end
  353. end
  354. magic = table.concat(magic_split, ' ')
  355. outStream:writeln('/// @' .. magic)
  356. fn_magic = checkComment4fn(fn_magic, magic)
  357. end
  358. elseif string.sub(line, 3, 3) == '-' then -- it's a nonmagic doc comment
  359. local comment = string.sub(line, 4)
  360. outStream:writeln('/// ' .. comment)
  361. elseif string.sub(line, 3, 4) == '[[' then -- it's a long comment
  362. line = string.sub(line, 5) -- nibble head
  363. local comment = ''
  364. local closeSquare, hitend, thisComment
  365. while not hitend and (not inStream:eof()) do
  366. closeSquare = string.find(line, ']]')
  367. if not closeSquare then -- need to look on another line
  368. thisComment = line .. '\n'
  369. line = inStream:getLine()
  370. else
  371. thisComment = string.sub(line, 1, closeSquare - 1)
  372. hitend = true
  373. -- unget the tail of the line
  374. -- in most cases it's empty. This may make us less efficient but
  375. -- easier to program
  376. inStream:ungetLine(string_trim(string.sub(line, closeSquare + 2)))
  377. end
  378. comment = comment .. thisComment
  379. end
  380. if string.sub(comment, 1, 1) == '@' then -- it's a long magic comment
  381. outStream:write('/*' .. comment .. '*/ ')
  382. fn_magic = checkComment4fn(fn_magic, comment)
  383. else -- discard
  384. outStream:write('/* zz:' .. comment .. '*/ ')
  385. fn_magic = nil
  386. end
  387. -- TODO(justinmk): Uncomment this if we want "--" lines to continue the
  388. -- preceding magic ("---", "--@", …) lines.
  389. -- elseif state == 'in_magic_comment' then -- next line of magic comment
  390. -- outStream:writeln('/// '.. line:sub(3))
  391. else -- discard
  392. outStream:writeln('// zz:"' .. line .. '"')
  393. fn_magic = nil
  394. end
  395. elseif string.find(line, '^function') or string.find(line, '^local%s+function') then
  396. generic = {}
  397. state = 'in_function' -- it's a function
  398. local pos_fn = string.find(line, 'function')
  399. -- function
  400. -- ....v...
  401. if pos_fn then
  402. -- we've got a function
  403. local fn = TString_removeCommentFromLine(string_trim(string.sub(line, pos_fn + 8)))
  404. if fn_magic then
  405. fn = fn_magic
  406. end
  407. if string.sub(fn, 1, 1) == '(' then
  408. -- it's an anonymous function
  409. outStream:writelnComment(line)
  410. else
  411. -- fn has a name, so is interesting
  412. -- want to fix for iffy declarations
  413. local open_paren = string.find(fn, '[%({]')
  414. if open_paren then
  415. -- we might have a missing close paren
  416. if not string.find(fn, '%)') then
  417. fn = fn .. ' ___MissingCloseParenHere___)'
  418. end
  419. end
  420. -- Big hax
  421. if string.find(fn, ':') then
  422. -- TODO: We need to add a first parameter of "SELF" here
  423. -- local colon_place = string.find(fn, ":")
  424. -- local name = string.sub(fn, 1, colon_place)
  425. fn = fn:gsub(':', '.', 1)
  426. outStream:writeln('/// @param self')
  427. local paren_start = string.find(fn, '(', 1, true)
  428. local paren_finish = string.find(fn, ')', 1, true)
  429. -- Nothing in between the parens
  430. local comma
  431. if paren_finish == paren_start + 1 then
  432. comma = ''
  433. else
  434. comma = ', '
  435. end
  436. fn = string.sub(fn, 1, paren_start)
  437. .. 'self'
  438. .. comma
  439. .. string.sub(fn, paren_start + 1)
  440. end
  441. -- add vanilla function
  442. outStream:writeln('function ' .. fn .. '{}')
  443. end
  444. else
  445. this:warning(inStream:getLineNo(), 'something weird here')
  446. end
  447. fn_magic = nil -- mustn't inadvertently use it again
  448. -- TODO: If we can make this learn how to generate these, that would be helpful.
  449. -- elseif string.find(line, "^M%['.*'%] = function") then
  450. -- state = 'in_function' -- it's a function
  451. -- outStream:writeln("function textDocument/publishDiagnostics(...){}")
  452. -- fn_magic = nil -- mustn't inadvertently use it again
  453. else
  454. state = '' -- unknown
  455. if #line > 0 then -- we don't know what this line means, so just comment it out
  456. outStream:writeln('// zz: ' .. line)
  457. else
  458. outStream:writeln() -- keep this line blank
  459. end
  460. end
  461. end
  462. -- output the tail
  463. outStream:write_tailLines()
  464. else
  465. outStream:writeln('!empty file')
  466. end
  467. end
  468. -- this application
  469. local TApp = class()
  470. -- constructor
  471. function TApp.init(this)
  472. this.timestamp = os.date('%c %Z', os.time())
  473. this.name = 'Lua2DoX'
  474. this.version = '0.2 20130128'
  475. this.copyright = 'Copyright (c) Simon Dales 2012-13'
  476. end
  477. function TApp.getRunStamp(this)
  478. return this.name .. ' (' .. this.version .. ') ' .. this.timestamp
  479. end
  480. function TApp.getVersion(this)
  481. return this.name .. ' (' .. this.version .. ') '
  482. end
  483. function TApp.getCopyright(this)
  484. return this.copyright
  485. end
  486. local This_app = TApp()
  487. --main
  488. if arg[1] == '--help' then
  489. TCore_IO_writeln(This_app:getVersion())
  490. TCore_IO_writeln(This_app:getCopyright())
  491. TCore_IO_writeln([[
  492. run as:
  493. nvim -l scripts/lua2dox.lua <param>
  494. --------------
  495. Param:
  496. <filename> : interprets filename
  497. --version : show version/copyright info
  498. --help : this help text]])
  499. elseif arg[1] == '--version' then
  500. TCore_IO_writeln(This_app:getVersion())
  501. TCore_IO_writeln(This_app:getCopyright())
  502. else -- It's a filter.
  503. local filename = arg[1]
  504. if arg[2] == '--outdir' then
  505. local outdir = arg[3]
  506. if type(outdir) ~= 'string' or (0 ~= vim.fn.filereadable(outdir) and 0 == vim.fn.isdirectory(outdir)) then
  507. error(('invalid --outdir: "%s"'):format(tostring(outdir)))
  508. end
  509. vim.fn.mkdir(outdir, 'p')
  510. _debug_outfile = string.format('%s/%s.c', outdir, vim.fs.basename(filename))
  511. end
  512. local appStamp = This_app:getRunStamp()
  513. local filter = TLua2DoX_filter()
  514. filter:filter(appStamp, filename)
  515. if _debug_outfile then
  516. local f = assert(io.open(_debug_outfile, 'w'))
  517. f:write(table.concat(_debug_output))
  518. f:close()
  519. end
  520. end
  521. --eof