main.lua 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. local f = io.popen('git branch --show-current')
  2. local dev = f and f:read('*a'):match('%w+') == 'dev'
  3. f:close()
  4. -- Helpers
  5. local function copy(t)
  6. if type(t) ~= 'table' then return t end
  7. local result = {}
  8. for k, v in pairs(t) do
  9. result[k] = copy(v)
  10. end
  11. return result
  12. end
  13. local function unindent(code)
  14. local indent = code:match('^(% +)')
  15. if indent then
  16. return code:gsub('\n' .. indent, '\n'):gsub('^' .. indent, ''):gsub('%s*$', '')
  17. else
  18. return code
  19. end
  20. end
  21. local function unwrap(str)
  22. if not str then return str end
  23. str = unindent(str)
  24. return str:gsub('([^\n]+)\n(%S)', function(a, b)
  25. if b == '-' or b == '>' or b == ':' or a:match(':::') then
  26. return a .. '\n' .. b
  27. else
  28. return a .. ' ' .. b
  29. end
  30. end)
  31. :gsub('^%s+', '')
  32. :gsub('%s+$', '')
  33. end
  34. local function pluralify(t, key)
  35. t[key .. 's'] = t[key .. 's'] or (t[key] and { t[key] } or nil)
  36. t[key] = nil
  37. return t[key .. 's']
  38. end
  39. local lookup = {}
  40. local function track(obj)
  41. lookup[obj.key] = obj
  42. end
  43. local function warn(s, ...)
  44. print(string.format(s, ...))
  45. end
  46. local function warnIf(cond, ...)
  47. if cond then warn(...) end
  48. end
  49. local function getVisibleDirectoryItems(path)
  50. local input = lovr.filesystem.getDirectoryItems(path)
  51. local output = {}
  52. for _, item in pairs(input) do
  53. if item:match('^%.') then
  54. warn('Skipping hidden file "%s"', item)
  55. else
  56. table.insert(output, item)
  57. end
  58. end
  59. return output
  60. end
  61. -- Processors
  62. local function processExample(example, key)
  63. if type(example) == 'string' then
  64. return {
  65. code = unindent(example)
  66. }
  67. else
  68. assert(example.code, string.format('%s example is missing code', key))
  69. example.description = unwrap(example.description)
  70. example.code = unindent(example.code)
  71. end
  72. return example
  73. end
  74. local function processEnum(path, parent)
  75. local enum = require(path)
  76. enum.name = path:match('[^/]+$')
  77. enum.key = enum.name
  78. enum.module = parent.key
  79. enum.description = unwrap(enum.description)
  80. enum.notes = unwrap(enum.notes)
  81. for _, value in ipairs(enum.values) do
  82. value.description = unwrap(value.description)
  83. end
  84. track(enum)
  85. return enum
  86. end
  87. local function processFunction(path, parent)
  88. local fn = require(path)
  89. fn.name = path:match('[^/]+$')
  90. fn.key = parent.name:match('^[A-Z]') and (parent.key .. ':' .. fn.name) or (path:gsub('/', '.'):gsub('callbacks%.', ''))
  91. fn.deprecated = type(fn.deprecated) == 'string' and unwrap(fn.deprecated) or fn.deprecated
  92. fn.description = unwrap(fn.description)
  93. fn.module = parent.module or parent.key
  94. fn.notes = unwrap(fn.notes)
  95. fn.examples = pluralify(fn, 'example')
  96. for k, example in ipairs(fn.examples or {}) do
  97. fn.examples[k] = processExample(example, fn.key)
  98. end
  99. assert(fn.variants and #fn.variants > 0, string.format('Function %q is missing variants', fn.key))
  100. assert(fn.arguments, string.format('Function %q does not have arguments list', fn.key))
  101. assert(fn.returns, string.format('Function %q does not have returns list', fn.key))
  102. for name, arg in pairs(fn.arguments) do
  103. arg.name = name
  104. end
  105. for name, ret in pairs(fn.returns) do
  106. ret.name = name
  107. end
  108. for i, variant in ipairs(fn.variants) do
  109. assert(variant.arguments, string.format('%q variant #%d is missing arguments', fn.key, i))
  110. assert(variant.returns, string.format('%q variant #%d is missing returns', fn.key, i))
  111. for j, name in ipairs(variant.arguments) do
  112. warnIf(not fn.arguments[name], string.format('%s uses unknown argument %s', fn.key, name))
  113. variant.arguments[j] = copy(fn.arguments[name])
  114. end
  115. for j, name in ipairs(variant.returns) do
  116. warnIf(not fn.returns[name], string.format('%s uses unknown return %s', fn.key, name))
  117. variant.returns[j] = copy(fn.returns[name])
  118. end
  119. end
  120. for _, variant in ipairs(fn.variants) do
  121. local function processTable(t)
  122. if not t then return end
  123. for _, field in ipairs(t) do
  124. field.description = unwrap(field.description)
  125. processTable(field.table)
  126. end
  127. t.description = unwrap(t.description)
  128. end
  129. variant.description = unwrap(variant.description)
  130. for _, arg in ipairs(variant.arguments) do
  131. arg.description = unwrap(arg.description)
  132. processTable(arg.table)
  133. end
  134. for _, ret in ipairs(variant.returns) do
  135. ret.description = unwrap(ret.description)
  136. processTable(ret.table)
  137. end
  138. end
  139. fn.arguments = nil
  140. fn.returns = nil
  141. track(fn)
  142. return fn
  143. end
  144. local function processObject(path, parent)
  145. local object = require(path .. '.init')
  146. assert(type(object) == 'table', string.format('%s/init.lua did not return a table', path))
  147. object.key = path:match('[^/]+$')
  148. object.name = object.key
  149. object.description = unwrap(object.description)
  150. object.summary = object.summary or object.description
  151. object.module = parent.key
  152. object.constructors = pluralify(object, 'constructor')
  153. object.notes = unwrap(object.notes)
  154. object.examples = pluralify(object, 'example')
  155. if object.sections then
  156. for _, section in ipairs(object.sections) do
  157. section.description = unwrap(section.description)
  158. end
  159. end
  160. local methods = {}
  161. for _, file in ipairs(getVisibleDirectoryItems(path)) do
  162. if file ~= 'init.lua' then
  163. local method = file:gsub('%..+$', '')
  164. local key = ('%s:%s'):format(object.name, method)
  165. methods[key] = processFunction(path .. '/' .. method, object)
  166. end
  167. end
  168. if object.methods then
  169. for i, key in ipairs(object.methods) do
  170. object.methods[i] = methods[key]
  171. warnIf(not methods[key], '%s links to unknown method %q', object.key, key)
  172. methods[key] = nil
  173. end
  174. for method in pairs(methods) do
  175. warn('%s is missing link to %q', object.key, method)
  176. end
  177. else
  178. object.methods = {}
  179. for key, method in pairs(methods) do
  180. table.insert(object.methods, method)
  181. end
  182. table.sort(object.methods, function(a, b) return a.key < b.key end)
  183. end
  184. for k, example in ipairs(object.examples or {}) do
  185. object.examples[k] = processExample(example, object.key)
  186. end
  187. track(object)
  188. return object
  189. end
  190. local function processModule(path)
  191. local module = require(path .. '.init') -- So we avoid requiring the module itself
  192. module.key = module.external and path:match('[^/]+$') or path:gsub('/', '.')
  193. module.name = module.external and module.key or module.key:match('[^%.]+$')
  194. module.description = unwrap(module.description)
  195. module.functions = {}
  196. module.objects = {}
  197. module.enums = {}
  198. module.notes = unwrap(module.notes)
  199. if module.sections then
  200. for _, section in ipairs(module.sections) do
  201. section.description = unwrap(section.description)
  202. end
  203. end
  204. module.examples = pluralify(module, 'example')
  205. for k, example in ipairs(module.examples or {}) do
  206. module.examples[k] = processExample(example, module.key)
  207. end
  208. for _, file in ipairs(getVisibleDirectoryItems(path)) do
  209. local childPath = path .. '/' .. file
  210. local childModule = childPath:gsub('%..+$', '')
  211. local isFile = lovr.filesystem.isFile(childPath)
  212. local capitalized = file:match('^[A-Z]')
  213. if file ~= 'init.lua' and not capitalized and isFile then
  214. table.insert(module.functions, processFunction(childModule, module))
  215. elseif capitalized and not isFile then
  216. table.insert(module.objects, processObject(childModule, module))
  217. elseif capitalized and isFile then
  218. table.insert(module.enums, processEnum(childModule, module))
  219. end
  220. end
  221. table.sort(module.functions, function(a, b) return a.key < b.key end)
  222. table.sort(module.objects, function(a, b) return a.key < b.key end)
  223. table.sort(module.enums, function(a, b) return a.key < b.key end)
  224. track(module)
  225. return module
  226. end
  227. -- Validation
  228. local function validateRelated(item)
  229. for _, key in ipairs(item.related or {}) do
  230. warnIf(not lookup[key], '%s has unknown related item %s', item.key, key)
  231. warnIf(key == item.key, '%s should not be related to itself', key)
  232. end
  233. end
  234. local function validateEnum(enum)
  235. warnIf(not enum.summary, 'Enum %s is missing summary', enum.name)
  236. warnIf(not enum.description, 'Enum %s is missing description', enum.name)
  237. for i, value in ipairs(enum.values) do
  238. warnIf(not value.name, 'Enum %s value #%d is missing name', enum.name, i)
  239. warnIf(not value.description, 'Enum %s value #%d is missing description', enum.name, i)
  240. end
  241. validateRelated(enum)
  242. end
  243. local function validateType(type, fields, key, kind)
  244. if not type then
  245. warn('Missing %s type in %s', kind, key)
  246. elseif type == 'table' and fields then
  247. for i, field in ipairs(fields) do
  248. validateType(field.type, field.table, key, kind)
  249. end
  250. else
  251. local valid = {
  252. ['nil'] = true,
  253. boolean = true,
  254. number = true,
  255. table = true,
  256. string = true,
  257. userdata = true,
  258. lightuserdata = true,
  259. ['function'] = true,
  260. ['*'] = true
  261. }
  262. for word in type:gmatch('%w+') do
  263. if word:match('^[A-Z]') then
  264. warnIf(not lookup[word], 'Invalid %s type "%s" in %s', kind, word, key)
  265. else
  266. warnIf(not valid[word], 'Invalid %s type "%s" in %s', kind, word, key)
  267. end
  268. end
  269. end
  270. end
  271. local function validateFunction(fn)
  272. if fn.tag then
  273. local found = false
  274. for _, section in ipairs(lookup[fn.module].sections or {}) do
  275. if section.tag == fn.tag then found = true break end
  276. end
  277. for _, object in ipairs(lookup[fn.module].objects) do
  278. for _, section in ipairs(object.sections or {}) do
  279. if section.tag == fn.tag then found = true break end
  280. end
  281. end
  282. warnIf(not found, 'Unknown tag %s for %s', fn.tag, fn.key)
  283. end
  284. for _, variant in ipairs(fn.variants) do
  285. for _, arg in ipairs(variant.arguments) do
  286. warnIf(not arg or not arg.name, 'Invalid argument for variant of %s', fn.key)
  287. validateType(arg.type, arg.table, fn.key, 'argument')
  288. end
  289. for _, ret in ipairs(variant.returns) do
  290. warnIf(not ret or not ret.name, 'Invalid return for variant of %s', fn.key)
  291. validateType(ret.type, ret.table, fn.key, 'return')
  292. end
  293. end
  294. validateRelated(fn)
  295. end
  296. local function validateObject(object)
  297. for _, constructor in ipairs(object.constructors or {}) do
  298. warnIf(not lookup[constructor], 'Constructor for %s not found: %s', object.key, constructor)
  299. end
  300. for _, method in ipairs(object.methods or {}) do
  301. validateFunction(method)
  302. if object.sections and not method.deprecated then
  303. local found = false
  304. for _, section in ipairs(object.sections) do
  305. if section.tag and section.tag == method.tag then
  306. found = true
  307. break
  308. else
  309. for _, link in ipairs(section.links or {}) do
  310. if link == method.key then
  311. found = true
  312. break
  313. end
  314. end
  315. end
  316. end
  317. warnIf(not found, '%s is missing from its parent\'s page', method.key)
  318. end
  319. end
  320. local metatable = debug.getregistry()[object.name]
  321. if dev and metatable then
  322. local hasMethod = {}
  323. for _, method in ipairs(object.methods or {}) do
  324. if not metatable[method.name] and not method.deprecated then
  325. warn('%s has docs for unknown method %s', object.name, method.name)
  326. end
  327. hasMethod[method.name] = true
  328. end
  329. if object.extends then
  330. for i, method in ipairs(lookup[object.extends].methods) do
  331. hasMethod[method.name] = true
  332. end
  333. end
  334. local ignore = {
  335. type = true,
  336. release = true,
  337. monkey = true
  338. }
  339. for name in pairs(metatable) do
  340. if not name:match('^__') and not ignore[name] then
  341. warnIf(not hasMethod[name], '%s is missing docs for %s', object.name, name)
  342. end
  343. end
  344. end
  345. validateRelated(object)
  346. end
  347. local function validateModule(module)
  348. local t = lovr[module.name]
  349. for _, object in ipairs(module.objects) do
  350. validateObject(object)
  351. end
  352. for _, fn in ipairs(module.functions) do
  353. validateFunction(fn)
  354. if dev and not fn.deprecated then
  355. warnIf(t and not t[fn.name], '%s has docs for unknown function %s', module.key, fn.name)
  356. end
  357. if module.sections and not fn.deprecated then
  358. local found = false
  359. for _, section in ipairs(module.sections) do
  360. if section.tag and section.tag == fn.tag then
  361. found = true
  362. break
  363. else
  364. for _, link in ipairs(section.links or {}) do
  365. if link == fn.key then
  366. found = true
  367. break
  368. end
  369. end
  370. end
  371. end
  372. warnIf(not found, '%s is missing a parent link/tag', fn.key)
  373. end
  374. end
  375. local ignore = {
  376. setSource = true,
  377. getBundlePath = true,
  378. openConsole = true
  379. }
  380. for name in pairs(t or {}) do
  381. local key = ('%s.%s'):format(module.key, name)
  382. warnIf(not ignore[name] and not lookup[key], 'Missing docs for %s', key)
  383. end
  384. for _, fn in ipairs(module.enums) do
  385. validateEnum(fn)
  386. end
  387. end
  388. function lovr.load()
  389. local api = {
  390. modules = {},
  391. callbacks = {}
  392. }
  393. -- So errhand exits
  394. lovr.graphics = nil
  395. -- Modules
  396. table.insert(api.modules, processModule('lovr'))
  397. for _, file in ipairs(lovr.filesystem.getDirectoryItems('lovr')) do
  398. local path = 'lovr/' .. file
  399. if file ~= 'callbacks' and file:match('^[a-z]') and lovr.filesystem.isDirectory(path) then
  400. table.insert(api.modules, processModule(path))
  401. end
  402. end
  403. -- Callbacks
  404. local callbacks = 'lovr/callbacks'
  405. for _, file in ipairs(getVisibleDirectoryItems(callbacks)) do
  406. table.insert(api.callbacks, processFunction(callbacks .. '/' .. file:gsub('%.lua', ''), api.modules[1]))
  407. end
  408. -- Validate
  409. for _, callback in ipairs(api.callbacks) do
  410. validateFunction(callback)
  411. end
  412. for _, module in ipairs(api.modules) do
  413. validateModule(module)
  414. end
  415. -- Sort
  416. table.sort(api.modules, function(a, b) return a.key < b.key end)
  417. table.sort(api.callbacks, function(a, b) return a.key < b.key end)
  418. -- Generate
  419. local ok, generator = pcall(require, 'generators.' .. (arg[1] or 'lua'))
  420. if not ok then
  421. print(('Could not load generator %q: %s'):format(name, generator))
  422. else
  423. generator(api)
  424. end
  425. -- Bye
  426. lovr.event.quit()
  427. end