luacats_parser.lua 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. local luacats_grammar = require('scripts.luacats_grammar')
  2. --- @class nvim.luacats.parser.param
  3. --- @field name string
  4. --- @field type string
  5. --- @field desc string
  6. --- @class nvim.luacats.parser.return
  7. --- @field name string
  8. --- @field type string
  9. --- @field desc string
  10. --- @class nvim.luacats.parser.note
  11. --- @field desc string
  12. --- @class nvim.luacats.parser.brief
  13. --- @field kind 'brief'
  14. --- @field desc string
  15. --- @class nvim.luacats.parser.alias
  16. --- @field kind 'alias'
  17. --- @field type string[]
  18. --- @field desc string
  19. --- @class nvim.luacats.parser.fun
  20. --- @field name string
  21. --- @field params nvim.luacats.parser.param[]
  22. --- @field returns nvim.luacats.parser.return[]
  23. --- @field desc string
  24. --- @field access? 'private'|'package'|'protected'
  25. --- @field class? string
  26. --- @field module? string
  27. --- @field modvar? string
  28. --- @field classvar? string
  29. --- @field deprecated? true
  30. --- @field since? string
  31. --- @field attrs? string[]
  32. --- @field nodoc? true
  33. --- @field generics? table<string,string>
  34. --- @field table? true
  35. --- @field notes? nvim.luacats.parser.note[]
  36. --- @field see? nvim.luacats.parser.note[]
  37. --- @class nvim.luacats.parser.field
  38. --- @field name string
  39. --- @field type string
  40. --- @field desc string
  41. --- @field access? 'private'|'package'|'protected'
  42. --- @class nvim.luacats.parser.class
  43. --- @field kind 'class'
  44. --- @field parent? string
  45. --- @field name string
  46. --- @field desc string
  47. --- @field nodoc? true
  48. --- @field inlinedoc? true
  49. --- @field access? 'private'|'package'|'protected'
  50. --- @field fields nvim.luacats.parser.field[]
  51. --- @field notes? string[]
  52. --- @class nvim.luacats.parser.State
  53. --- @field doc_lines? string[]
  54. --- @field cur_obj? nvim.luacats.parser.obj
  55. --- @field last_doc_item? nvim.luacats.parser.param|nvim.luacats.parser.return|nvim.luacats.parser.note
  56. --- @field last_doc_item_indent? integer
  57. --- @alias nvim.luacats.parser.obj
  58. --- | nvim.luacats.parser.class
  59. --- | nvim.luacats.parser.fun
  60. --- | nvim.luacats.parser.brief
  61. --- | nvim.luacats.parser.alias
  62. -- Remove this when we document classes properly
  63. --- Some doc lines have the form:
  64. --- param name some.complex.type (table) description
  65. --- if so then transform the line to remove the complex type:
  66. --- param name (table) description
  67. --- @param line string
  68. local function use_type_alt(line)
  69. for _, type in ipairs({ 'table', 'function' }) do
  70. line = line:gsub('@param%s+([a-zA-Z_?]+)%s+.*%((' .. type .. ')%)', '@param %1 %2')
  71. line = line:gsub('@param%s+([a-zA-Z_?]+)%s+.*%((' .. type .. '|nil)%)', '@param %1 %2')
  72. line = line:gsub('@param%s+([a-zA-Z_?]+)%s+.*%((' .. type .. '%?)%)', '@param %1 %2')
  73. line = line:gsub('@return%s+.*%((' .. type .. ')%)', '@return %1')
  74. line = line:gsub('@return%s+.*%((' .. type .. '|nil)%)', '@return %1')
  75. line = line:gsub('@return%s+.*%((' .. type .. '%?)%)', '@return %1')
  76. end
  77. return line
  78. end
  79. --- If we collected any `---` lines. Add them to the existing (or new) object
  80. --- Used for function/class descriptions and multiline param descriptions.
  81. --- @param state nvim.luacats.parser.State
  82. local function add_doc_lines_to_obj(state)
  83. if state.doc_lines then
  84. state.cur_obj = state.cur_obj or {}
  85. local cur_obj = assert(state.cur_obj)
  86. local txt = table.concat(state.doc_lines, '\n')
  87. if cur_obj.desc then
  88. cur_obj.desc = cur_obj.desc .. '\n' .. txt
  89. else
  90. cur_obj.desc = txt
  91. end
  92. state.doc_lines = nil
  93. end
  94. end
  95. --- @param line string
  96. --- @param state nvim.luacats.parser.State
  97. local function process_doc_line(line, state)
  98. line = line:sub(4):gsub('^%s+@', '@')
  99. line = use_type_alt(line)
  100. local parsed = luacats_grammar:match(line)
  101. if not parsed then
  102. if line:match('^ ') then
  103. line = line:sub(2)
  104. end
  105. if state.last_doc_item then
  106. if not state.last_doc_item_indent then
  107. state.last_doc_item_indent = #line:match('^%s*') + 1
  108. end
  109. state.last_doc_item.desc = (state.last_doc_item.desc or '')
  110. .. '\n'
  111. .. line:sub(state.last_doc_item_indent or 1)
  112. else
  113. state.doc_lines = state.doc_lines or {}
  114. table.insert(state.doc_lines, line)
  115. end
  116. return
  117. end
  118. state.last_doc_item_indent = nil
  119. state.last_doc_item = nil
  120. state.cur_obj = state.cur_obj or {}
  121. local cur_obj = assert(state.cur_obj)
  122. local kind = parsed.kind
  123. if kind == 'brief' then
  124. state.cur_obj = {
  125. kind = 'brief',
  126. desc = parsed.desc,
  127. }
  128. elseif kind == 'class' then
  129. --- @cast parsed nvim.luacats.Class
  130. cur_obj.kind = 'class'
  131. cur_obj.name = parsed.name
  132. cur_obj.parent = parsed.parent
  133. cur_obj.access = parsed.access
  134. cur_obj.desc = state.doc_lines and table.concat(state.doc_lines, '\n') or nil
  135. state.doc_lines = nil
  136. cur_obj.fields = {}
  137. elseif kind == 'field' then
  138. --- @cast parsed nvim.luacats.Field
  139. parsed.desc = parsed.desc or state.doc_lines and table.concat(state.doc_lines, '\n') or nil
  140. if parsed.desc then
  141. parsed.desc = vim.trim(parsed.desc)
  142. end
  143. table.insert(cur_obj.fields, parsed)
  144. state.doc_lines = nil
  145. elseif kind == 'operator' then
  146. parsed.desc = parsed.desc or state.doc_lines and table.concat(state.doc_lines, '\n') or nil
  147. if parsed.desc then
  148. parsed.desc = vim.trim(parsed.desc)
  149. end
  150. table.insert(cur_obj.fields, parsed)
  151. state.doc_lines = nil
  152. elseif kind == 'param' then
  153. state.last_doc_item_indent = nil
  154. cur_obj.params = cur_obj.params or {}
  155. if vim.endswith(parsed.name, '?') then
  156. parsed.name = parsed.name:sub(1, -2)
  157. parsed.type = parsed.type .. '?'
  158. end
  159. state.last_doc_item = {
  160. name = parsed.name,
  161. type = parsed.type,
  162. desc = parsed.desc,
  163. }
  164. table.insert(cur_obj.params, state.last_doc_item)
  165. elseif kind == 'return' then
  166. cur_obj.returns = cur_obj.returns or {}
  167. for _, t in ipairs(parsed) do
  168. table.insert(cur_obj.returns, {
  169. name = t.name,
  170. type = t.type,
  171. desc = parsed.desc,
  172. })
  173. end
  174. state.last_doc_item_indent = nil
  175. state.last_doc_item = cur_obj.returns[#cur_obj.returns]
  176. elseif kind == 'private' then
  177. cur_obj.access = 'private'
  178. elseif kind == 'package' then
  179. cur_obj.access = 'package'
  180. elseif kind == 'protected' then
  181. cur_obj.access = 'protected'
  182. elseif kind == 'deprecated' then
  183. cur_obj.deprecated = true
  184. elseif kind == 'inlinedoc' then
  185. cur_obj.inlinedoc = true
  186. elseif kind == 'nodoc' then
  187. cur_obj.nodoc = true
  188. elseif kind == 'since' then
  189. cur_obj.since = parsed.desc
  190. elseif kind == 'see' then
  191. cur_obj.see = cur_obj.see or {}
  192. table.insert(cur_obj.see, { desc = parsed.desc })
  193. elseif kind == 'note' then
  194. state.last_doc_item_indent = nil
  195. state.last_doc_item = {
  196. desc = parsed.desc,
  197. }
  198. cur_obj.notes = cur_obj.notes or {}
  199. table.insert(cur_obj.notes, state.last_doc_item)
  200. elseif kind == 'type' then
  201. cur_obj.desc = parsed.desc
  202. parsed.desc = nil
  203. parsed.kind = nil
  204. cur_obj.type = parsed
  205. elseif kind == 'alias' then
  206. state.cur_obj = {
  207. kind = 'alias',
  208. desc = parsed.desc,
  209. }
  210. elseif kind == 'enum' then
  211. -- TODO
  212. state.doc_lines = nil
  213. elseif
  214. vim.tbl_contains({
  215. 'diagnostic',
  216. 'cast',
  217. 'overload',
  218. 'meta',
  219. }, kind)
  220. then
  221. -- Ignore
  222. return
  223. elseif kind == 'generic' then
  224. cur_obj.generics = cur_obj.generics or {}
  225. cur_obj.generics[parsed.name] = parsed.type or 'any'
  226. else
  227. error('Unhandled' .. vim.inspect(parsed))
  228. end
  229. end
  230. --- @param fun nvim.luacats.parser.fun
  231. --- @return nvim.luacats.parser.field
  232. local function fun2field(fun)
  233. local parts = { 'fun(' }
  234. for _, p in ipairs(fun.params or {}) do
  235. parts[#parts + 1] = string.format('%s: %s', p.name, p.type)
  236. end
  237. parts[#parts + 1] = ')'
  238. if fun.returns then
  239. parts[#parts + 1] = ': '
  240. local tys = {} --- @type string[]
  241. for _, p in ipairs(fun.returns) do
  242. tys[#tys + 1] = p.type
  243. end
  244. parts[#parts + 1] = table.concat(tys, ', ')
  245. end
  246. return {
  247. name = fun.name,
  248. type = table.concat(parts, ''),
  249. access = fun.access,
  250. desc = fun.desc,
  251. }
  252. end
  253. --- Function to normalize known form for declaring functions and normalize into a more standard
  254. --- form.
  255. --- @param line string
  256. --- @return string
  257. local function filter_decl(line)
  258. -- M.fun = vim._memoize(function(...)
  259. -- ->
  260. -- function M.fun(...)
  261. line = line:gsub('^local (.+) = memoize%([^,]+, function%((.*)%)$', 'local function %1(%2)')
  262. line = line:gsub('^(.+) = memoize%([^,]+, function%((.*)%)$', 'function %1(%2)')
  263. return line
  264. end
  265. --- @param line string
  266. --- @param state nvim.luacats.parser.State
  267. --- @param classes table<string,nvim.luacats.parser.class>
  268. --- @param classvars table<string,string>
  269. --- @param has_indent boolean
  270. local function process_lua_line(line, state, classes, classvars, has_indent)
  271. line = filter_decl(line)
  272. if state.cur_obj and state.cur_obj.kind == 'class' then
  273. local nm = line:match('^local%s+([a-zA-Z0-9_]+)%s*=')
  274. if nm then
  275. classvars[nm] = state.cur_obj.name
  276. end
  277. return
  278. end
  279. do
  280. local parent_tbl, sep, fun_or_meth_nm =
  281. line:match('^function%s+([a-zA-Z0-9_]+)([.:])([a-zA-Z0-9_]+)%s*%(')
  282. if parent_tbl then
  283. -- Have a decl. Ensure cur_obj
  284. state.cur_obj = state.cur_obj or {}
  285. local cur_obj = assert(state.cur_obj)
  286. -- Match `Class:foo` methods for defined classes
  287. local class = classvars[parent_tbl]
  288. if class then
  289. --- @cast cur_obj nvim.luacats.parser.fun
  290. cur_obj.name = fun_or_meth_nm
  291. cur_obj.class = class
  292. cur_obj.classvar = parent_tbl
  293. -- Add self param to methods
  294. if sep == ':' then
  295. cur_obj.params = cur_obj.params or {}
  296. table.insert(cur_obj.params, 1, {
  297. name = 'self',
  298. type = class,
  299. })
  300. end
  301. -- Add method as the field to the class
  302. table.insert(classes[class].fields, fun2field(cur_obj))
  303. return
  304. end
  305. -- Match `M.foo`
  306. if cur_obj and parent_tbl == cur_obj.modvar then
  307. cur_obj.name = fun_or_meth_nm
  308. return
  309. end
  310. end
  311. end
  312. do
  313. -- Handle: `function A.B.C.foo(...)`
  314. local fn_nm = line:match('^function%s+([.a-zA-Z0-9_]+)%s*%(')
  315. if fn_nm then
  316. state.cur_obj = state.cur_obj or {}
  317. state.cur_obj.name = fn_nm
  318. return
  319. end
  320. end
  321. do
  322. -- Handle: `M.foo = {...}` where `M` is the modvar
  323. local parent_tbl, tbl_nm = line:match('([a-zA-Z_]+)%.([a-zA-Z0-9_]+)%s*=')
  324. if state.cur_obj and parent_tbl and parent_tbl == state.cur_obj.modvar then
  325. state.cur_obj.name = tbl_nm
  326. state.cur_obj.table = true
  327. return
  328. end
  329. end
  330. do
  331. -- Handle: `foo = {...}`
  332. local tbl_nm = line:match('^([a-zA-Z0-9_]+)%s*=')
  333. if tbl_nm and not has_indent then
  334. state.cur_obj = state.cur_obj or {}
  335. state.cur_obj.name = tbl_nm
  336. state.cur_obj.table = true
  337. return
  338. end
  339. end
  340. do
  341. -- Handle: `vim.foo = {...}`
  342. local tbl_nm = line:match('^(vim%.[a-zA-Z0-9_]+)%s*=')
  343. if state.cur_obj and tbl_nm and not has_indent then
  344. state.cur_obj.name = tbl_nm
  345. state.cur_obj.table = true
  346. return
  347. end
  348. end
  349. if state.cur_obj then
  350. if line:find('^%s*%-%- luacheck:') then
  351. state.cur_obj = nil
  352. elseif line:find('^%s*local%s+') then
  353. state.cur_obj = nil
  354. elseif line:find('^%s*return%s+') then
  355. state.cur_obj = nil
  356. elseif line:find('^%s*[a-zA-Z_.]+%(%s+') then
  357. state.cur_obj = nil
  358. end
  359. end
  360. end
  361. --- Determine the table name used to export functions of a module
  362. --- Usually this is `M`.
  363. --- @param str string
  364. --- @return string?
  365. local function determine_modvar(str)
  366. local modvar --- @type string?
  367. for line in vim.gsplit(str, '\n') do
  368. do
  369. --- @type string?
  370. local m = line:match('^return%s+([a-zA-Z_]+)')
  371. if m then
  372. modvar = m
  373. end
  374. end
  375. do
  376. --- @type string?
  377. local m = line:match('^return%s+setmetatable%(([a-zA-Z_]+),')
  378. if m then
  379. modvar = m
  380. end
  381. end
  382. end
  383. return modvar
  384. end
  385. --- @param obj nvim.luacats.parser.obj
  386. --- @param funs nvim.luacats.parser.fun[]
  387. --- @param classes table<string,nvim.luacats.parser.class>
  388. --- @param briefs string[]
  389. --- @param uncommitted nvim.luacats.parser.obj[]
  390. local function commit_obj(obj, classes, funs, briefs, uncommitted)
  391. local commit = false
  392. if obj.kind == 'class' then
  393. --- @cast obj nvim.luacats.parser.class
  394. if not classes[obj.name] then
  395. classes[obj.name] = obj
  396. commit = true
  397. end
  398. elseif obj.kind == 'alias' then
  399. -- Just pretend
  400. commit = true
  401. elseif obj.kind == 'brief' then
  402. --- @cast obj nvim.luacats.parser.brief`
  403. briefs[#briefs + 1] = obj.desc
  404. commit = true
  405. else
  406. --- @cast obj nvim.luacats.parser.fun`
  407. if obj.name then
  408. funs[#funs + 1] = obj
  409. commit = true
  410. end
  411. end
  412. if not commit then
  413. table.insert(uncommitted, obj)
  414. end
  415. return commit
  416. end
  417. --- @param filename string
  418. --- @param uncommitted nvim.luacats.parser.obj[]
  419. -- luacheck: no unused
  420. local function dump_uncommitted(filename, uncommitted)
  421. local out_path = 'luacats-uncommited/' .. filename:gsub('/', '%%') .. '.txt'
  422. if #uncommitted > 0 then
  423. print(string.format('Could not commit %d objects in %s', #uncommitted, filename))
  424. vim.fn.mkdir(assert(vim.fs.dirname(out_path)), 'p')
  425. local f = assert(io.open(out_path, 'w'))
  426. for i, x in ipairs(uncommitted) do
  427. f:write(i)
  428. f:write(': ')
  429. f:write(vim.inspect(x))
  430. f:write('\n')
  431. end
  432. f:close()
  433. else
  434. vim.fn.delete(out_path)
  435. end
  436. end
  437. local M = {}
  438. function M.parse_str(str, filename)
  439. local funs = {} --- @type nvim.luacats.parser.fun[]
  440. local classes = {} --- @type table<string,nvim.luacats.parser.class>
  441. local briefs = {} --- @type string[]
  442. local mod_return = determine_modvar(str)
  443. --- @type string
  444. local module = filename:match('.*/lua/([a-z_][a-z0-9_/]+)%.lua') or filename
  445. module = module:gsub('/', '.')
  446. local classvars = {} --- @type table<string,string>
  447. local state = {} --- @type nvim.luacats.parser.State
  448. -- Keep track of any partial objects we don't commit
  449. local uncommitted = {} --- @type nvim.luacats.parser.obj[]
  450. for line in vim.gsplit(str, '\n') do
  451. local has_indent = line:match('^%s+') ~= nil
  452. line = vim.trim(line)
  453. if vim.startswith(line, '---') then
  454. process_doc_line(line, state)
  455. else
  456. add_doc_lines_to_obj(state)
  457. if state.cur_obj then
  458. state.cur_obj.modvar = mod_return
  459. state.cur_obj.module = module
  460. end
  461. process_lua_line(line, state, classes, classvars, has_indent)
  462. -- Commit the object
  463. local cur_obj = state.cur_obj
  464. if cur_obj then
  465. if not commit_obj(cur_obj, classes, funs, briefs, uncommitted) then
  466. --- @diagnostic disable-next-line:inject-field
  467. cur_obj.line = line
  468. end
  469. end
  470. state = {}
  471. end
  472. end
  473. -- dump_uncommitted(filename, uncommitted)
  474. return classes, funs, briefs, uncommitted
  475. end
  476. --- @param filename string
  477. function M.parse(filename)
  478. local f = assert(io.open(filename, 'r'))
  479. local txt = f:read('*all')
  480. f:close()
  481. return M.parse_str(txt, filename)
  482. end
  483. return M