Merge pull request 'Move to an AST like data structure' (#76) from feat/refactor-ast into master
ci/woodpecker/push/docker-build Pipeline was successful Details
ci/woodpecker/push/lint Pipeline failed Details
ci/woodpecker/push/build Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details

Reviewed-on: #76
This commit is contained in:
Aire-One 2023-08-03 21:03:11 +02:00
commit 4dd8d433f4
29 changed files with 1920 additions and 367 deletions

View File

@ -1,3 +1,7 @@
if os.getenv "LOCAL_LUA_DEBUGGER_VSCODE" == "1" then
require("lldebugger").start()
end
require("tl").loader()
local version <const> = _VERSION:match "%d+%.%d+"
@ -10,11 +14,14 @@ local function lua_module_paths(module_base_path)
end
return {
default = {
_all = {
lpath = lua_module_paths("lua_modules/share/lua/" .. version)
.. lua_module_paths "types"
.. lua_module_paths "./src/awesomewm.d.tl",
cpath = "lua_modules/lib/lua/" .. version .. "/?.so;",
loaders = { "teal" },
},
coverage = {
coverage = true,
},
}

View File

@ -8,13 +8,7 @@ charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.lua]
indent_size = 3
[*.{tl,d.tl}]
indent_size = 3
[.busted]
[*.{lua,tl,d.tl,busted,luacheckrc,luacov}]
indent_size = 3
[justfile]

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
lua_modules
generated
build
luacov

46
.luacov Normal file
View File

@ -0,0 +1,46 @@
-- -- Per https://github.com/lunarmodules/luacov/blob/c915f4b95e8df5dbfaf2b1f04f0b7da04506fc7a/src/luacov/reporter.lua#L102
-- -- > The option includeuntestedfiles requires the lfs module (from luafilesystem) to be installed.
-- -- So I'm ok with including it here, since it's already an hard dependency.
-- local lfs = require "lfs"
-- local function walk(dir, callback)
-- for file in lfs.dir(dir) do
-- if file ~= "." and file ~= ".." then
-- local path = dir .. "/" .. file
-- local attr = lfs.attributes(path)
-- if attr.mode == "directory" then
-- walk(path, callback)
-- else
-- callback(path)
-- end
-- end
-- end
-- end
-- local teal_files = {}
-- for _, path in pairs { "spec", "src" } do
-- walk(path, function(file)
-- if file:match "%.tl$" then
-- table.insert(teal_files, file)
-- end
-- end)
-- end
return {
statsfile = "luacov/luacov.stats.out",
reporter = "html",
reportfile = "luacov/luacov.report.html",
include = {
"spec",
"src",
},
exclude = {
"lua_modules",
"spec/tlconfig.lua",
"src/awesome.d.tl/types",
"types",
},
-- runreport = true, -- For some reason, it makes my computer crash
-- includeuntestedfiles = teal_files,
includeuntestedfiles = true,
}

16
.vscode/launch.json vendored
View File

@ -8,7 +8,21 @@
"program": {
"command": "just"
},
"args": ["debug"]
"args": ["run"],
"scriptFiles": ["${workspaceFolder}/**/*.tl"]
},
{
"name": "Debug specs",
"type": "lua-local",
"request": "launch",
"program": {
"command": "just"
},
"args": [
"test",
"_spec" // Adapt to the spec to debug
],
"scriptFiles": ["${workspaceFolder}/**/*.tl"]
}
]
}

25
.vscode/settings.json vendored
View File

@ -1,10 +1,8 @@
{
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"[markdown]": {
"editor.wordWrap": "on",
"editor.acceptSuggestionOnEnter": "off",
"editor.renderWhitespace": "all",
"editor.acceptSuggestionOnEnter": "off"
"editor.wordWrap": "on"
},
"cSpell.words": [
"aire-one",
@ -13,10 +11,12 @@
"ansicolors",
"awesomewm",
"buildx",
"concat",
"dryrun",
"getcontent",
"gitea",
"htmlparser",
"includeuntestedfiles",
"isdir",
"justfile",
"libc",
@ -24,24 +24,37 @@
"lpath",
"Luacheck",
"luacheckrc",
"luacov",
"luafilesystem",
"lualogging",
"Luarocks",
"luasec",
"luasocket",
"mkdir",
"modname",
"reportfile",
"rockspec",
"rstrip",
"runreport",
"setopt",
"statsfile",
"stringx",
"Stylua",
"sublist",
"tablex",
"tbody",
"tmpl",
"wibox",
"woodpeckerci",
"writefunction"
],
"debug.allowBreakpointsEverywhere": true,
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"files.associations": {
".busted": "lua",
".luacheckrc": "lua",
"*.rockspec": "lua",
".busted": "lua"
".luacov": "lua",
"*.rockspec": "lua"
}
}

View File

@ -47,13 +47,10 @@ validate:
awesomerc.tl
# `find . -type f -iname '*.d.tl' | xargs`
test:
busted
test PATTERN="_spec":
busted --pattern={{ PATTERN }}
# TODO : how to run a debugger on Teal code?
debug:
{{ lua }} debug.lua build/awesomewm.d.tl/init.lua
debug-gen:
cyan gen --output set_paths.lua --gen-compat "off" set_paths.tl
sed -i 's/"src\/awesomewm.d.tl"/"build\/awesomewm.d.tl"/g' set_paths.lua
stylua set_paths.lua
# Requires a patched version of luacov (https://github.com/lunarmodules/luacov/issues/98#issuecomment-1530491759)
coverage:
busted --coverage
luacov

View File

@ -1,30 +0,0 @@
local assert = require("luassert")
describe("Busted unit testing framework", function()
describe("should be awesome", function()
it("should be easy to use", function()
assert.truthy "Yup."
end)
it("should have lots of features", function()
-- deep check comparisons!
assert.same({ table = "great" }, { table = "great" })
-- or check by reference!
assert.not_equal({ table = "great" }, { table = "great" })
assert.truthy "this is a string" -- truthy: not false or nil
assert.is_true(1 == 1)
assert.falsy(nil)
assert.has_error(function()
error "Wat"
end, "Wat")
end)
it("should provide some shortcuts to common functions", function()
assert.is_unique { { thing = 1 }, { thing = 2 }, { thing = 3 } }
end)
end)
end)

View File

@ -1,10 +0,0 @@
local assert = require("luassert")
local utils = require("utils")
describe("test", function()
it("has_item", function()
local t = {1, 2, 3}
assert.equal(utils.has_item(t, 1), 1)
assert.is_nil(utils.has_item(t, 4))
end)
end)

View File

@ -0,0 +1,174 @@
local assert = require("luassert")
local type Node = require("types.Node")
local stringx = require("pl.stringx")
local teal_type_definition_printer = require("printer.teal_type_definition")
local printer = teal_type_definition_printer.printer
-- We need to remove the last newline inserted by Penlight's dedent
local function dedent(str: string): string
return (stringx.dedent(str):sub(1, -2))
end
local function gen(ast: Node, expected_code: string): function()
return function()
local generated = printer(ast)
assert.same(dedent(expected_code), generated)
end
end
describe("Teal type definition Printer", function()
it("should print a simple module type definition", gen(
{
children = {},
dependencies = {},
name = "Empty",
token = "module",
},
[[
-- This file was auto-generated.
local record Empty
end
return Empty
]]))
it("should print an Enum exposed by the module", gen(
{
children = {
{
children = {
{
name = "widget::layout_changed",
token = "identifier",
},
{
name = "widget::redraw_needed",
token = "identifier",
},
},
name = "Signal",
token = "enum",
},
},
dependencies = {},
name = "Signal_Module",
token = "module",
},
[[
-- This file was auto-generated.
local record Signal_Module
enum Signal
"widget::layout_changed"
"widget::redraw_needed"
end
end
return Signal_Module
]]))
it("should print a property exposed by the module", gen(
{
children = {
{
name = "text",
types = { "string", "nil" },
token = "variable",
}
},
dependencies = {},
name = "Property_Module",
token = "module",
},
[[
-- This file was auto-generated.
local record Property_Module
text: string | nil
end
return Property_Module
]]))
it("should print a function exposed by the module", gen(
{
children = {
{
parameters = {
{
types = { "integer" },
name = "pid",
token = "variable",
},
{
types = { "integer" },
name = "sig",
token = "variable",
},
},
return_types = { "boolean" },
name = "kill",
token = "function",
},
},
dependencies = {},
name = "Function_Module",
token = "module",
},
[[
-- This file was auto-generated.
local record Function_Module
kill: function(pid: integer, sig: integer): boolean
end
return Function_Module
]]))
it("should print nested Record exposed by the module", gen(
{
children = {
{
children = {},
name = "Nested",
token = "record",
}
},
dependencies = {},
name = "Nested_Module",
token = "module",
},
[[
-- This file was auto-generated.
local record Nested_Module
record Nested
end
end
return Nested_Module
]]))
it("should print require statement for module's dependencies", gen(
{
children = {},
dependencies = {
dep = "dep",
deeper = "path.dep.deeper",
},
name = "Module",
token = "module",
},
[[
-- This file was auto-generated.
local type deeper = require("path.dep.deeper")
local type dep = require("dep")
local record Module
end
return Module
]]))
end)

View File

@ -0,0 +1,823 @@
local assert = require("luassert")
local type Node = require("types.Node")
local scraper = require("scraper.module_doc")
local get_doc_from_page = scraper.get_doc_from_page
local function test(html: string, module_path: string, expected_ast: Node, expected_other_nodes: { Node } | nil): function()
return function()
local ast <const>, other_nodes <const> = get_doc_from_page(html, module_path)
assert.same(expected_ast, ast)
assert.same(expected_other_nodes or {}, other_nodes)
end
end
describe("Scrap documentation", function()
it("should return a valid AST for an empty module", test(
"",
"empty",
{
children = {
{
children = {},
name = "Signal",
token = "enum",
}
},
name = "Empty",
module_path = "empty",
dependencies = {},
token = "module",
}))
it("should produce Variable and `property::` Signal nodes", test(
[[
<h2 class="section-header">
<a name="Object_properties"></a>Object properties
</h2>
<dl class="function">
<dt>
<a class="copy-link js-copy-link" name="value" href="#value">🔗</a>
<strong>value</strong>
<span class="proptype"><span class="summary_type">number</span></span>
<span class="baseclass"> · 1 signal </span>
</dt>
<dd>
<h3>Constraints:</h3>
<span class="property_type">
<table class="see_also">
<tbody><tr class="">
<td style="padding-left:0px;">
<i>
Default value
</i>
</td>
<td>: <code>0</code></td>
</tr><tr>
</tr><tr class="">
<td style="padding-left:0px;">
<i>
Negative allowed
</i>
</td>
<td>: true</td>
</tr><tr>
</tr></tbody></table>
</span>
</dd>
</dl>
]],
"property_signal",
{
children = {
{
children = {
{
name = "property::value",
token = "identifier",
},
},
name = "Signal",
token = "enum",
},
{
name = "value",
types = { "number" },
token = "variable",
}
},
name = "Property_signal",
module_path = "property_signal",
dependencies = {},
token = "module",
}))
it("should produce Enum nodes when an Object Property type is a String with constraints", test(
[[
<h2 class="section-header">
<a name="Object_properties"></a>Object properties
</h2>
<dl class="function">
<dt>
<a
class="copy-link js-copy-link"
name="horizontal_fit_policy"
href="#horizontal_fit_policy"
>🔗</a
>
<strong>horizontal_fit_policy</strong>
<span class="proptype"><span class="summary_type">string</span></span>
<span class="baseclass"> · 1 signal </span>
</dt>
<dd>
<span class="property_type">
<table class="see_also">
<tbody>
<tr class="">
<td style="padding-left: 0px">
<i> Default value </i>
</td>
<td>: <code>"auto"</code></td>
</tr>
<tr></tr>
<tr class="">
<td style="padding-left: 0px">
<i> Valid values: </i>
</td>
</tr>
<tr></tr>
<tr class="see_also_sublist">
<td style="padding-left: 15px">
<i>
<code>"auto"</code>
</i>
</td>
<td>
: Honor the <code>resize</code> variable and preserve the aspect
ratio.
</td>
</tr>
<tr></tr>
<tr class="see_also_sublist">
<td style="padding-left: 15px">
<i>
<code>"none"</code>
</i>
</td>
<td>: Do not resize at all.</td>
</tr>
<tr></tr>
<tr class="see_also_sublist">
<td style="padding-left: 15px">
<i>
<code>"fit"</code>
</i>
</td>
<td>: Resize to the widget width.</td>
</tr>
<tr></tr>
</tbody>
</table>
</span>
</dd>
</dl>
]],
"property_enum",
{
children = {
{
children = {
{
name = "property::horizontal_fit_policy",
token = "identifier",
},
},
name = "Signal",
token = "enum",
},
{
children = {
{
name = "auto",
token = "identifier",
},
{
name = "none",
token = "identifier",
},
{
name = "fit",
token = "identifier",
},
},
name = "Horizontal_fit_policy",
token = "enum",
},
{
name = "horizontal_fit_policy",
types = { "Horizontal_fit_policy" },
token = "variable",
},
},
name = "Property_enum",
module_path = "property_enum",
dependencies = {},
token = "module",
}))
it("should produce a `string` typed Variable node when a String Property has no constraint", test(
[[
<h2 class="section-header">
<a name="Object_properties"></a>Object properties
</h2>
<dl class="function">
<dt>
<a class="copy-link js-copy-link" name="markup" href="#markup">🔗</a>
<strong>markup</strong>
<span class="proptype"><span class="summary_type">string</span></span>
<span class="baseclass"> · 1 signal </span>
</dt>
<dd>
<span class="property_type">string</span>
</dd>
</dl>
]],
"property_string",
{
children = {
{
children = {
{
name = "property::markup",
token = "identifier",
},
},
name = "Signal",
token = "enum",
},
{
name = "markup",
types = { "string" },
token = "variable",
}
},
name = "Property_string",
module_path = "property_string",
dependencies = {},
token = "module",
}))
it("should provide a Function node with the `self` as the first positional parameter", test(
[[
<h2 class="section-header"><a name="Object_methods"></a>Object methods</h2>
<dl class="function">
<dt>
<a class="copy-link js-copy-link" name="swap" href="#swap">🔗</a>
<strong
>:swap <span class="function_args"> <b>(</b>tag2<b>)</b></span></strong
>
<span class="proptype"></span>
<span class="baseclass"> </span>
</dt>
<dd>
<h3>Parameters:</h3>
<table class="see_also">
<tbody>
<tr class="param_header">
<th>Name</th>
<th></th>
<th>Type(s)</th>
<th>Description</th>
</tr>
<tr>
<td><span class="parameter">tag2</span></td>
<td></td>
<td>
<span class="types"><span class="type">tag</span></span>
</td>
<td class="see_also_description">The second tag</td>
</tr>
</tbody>
</table>
</dd>
</dl>
]],
"awful.tag",
{
children = {
{
children = {},
name = "Signal",
token = "enum",
},
{
parameters = {
{
types = { "Tag" },
name = "self",
token = "variable",
},
{
types = { "tag" }, -- This needs to be fixed : tag -> Tag
name = "tag2",
token = "variable",
}
},
return_types = {},
name = "swap",
token = "function",
},
},
name = "Tag",
module_path = "awful.tag",
dependencies = {},
token = "module",
}))
it("should produce Signal nodes", test(
[[
<h2 class="section-header"><a name="Signals"></a>Signals</h2>
<dl class="function">
<dt>
<a
class="copy-link js-copy-link"
name="widget::layout_changed"
href="#widget::layout_changed"
>🔗</a
>
<strong>widget::layout_changed</strong>
<span class="baseclass"> ·&nbsp;Inherited from wibox.widget.base </span>
</dt>
<dd></dd>
<dt>
<a
class="copy-link js-copy-link"
name="widget::redraw_needed"
href="#widget::redraw_needed"
>🔗</a
>
<strong>widget::redraw_needed</strong>
<span class="baseclass"> ·&nbsp;Inherited from wibox.widget.base </span>
</dt>
<dd></dd>
</dl>
]],
"signal",
{
children = {
{
children = {
{
name = "widget::layout_changed",
token = "identifier",
},
{
name = "widget::redraw_needed",
token = "identifier",
},
},
name = "Signal",
token = "enum",
},
},
name = "Signal",
module_path = "signal",
dependencies = {},
token = "module",
}))
it("should produce Function nodes", test(
[[
<h2 class="section-header">
<a name="Static_module_functions"></a>Static module functions
</h2>
<dl class="function">
<dt>
<a class="copy-link js-copy-link" name="kill" href="#kill">🔗</a>
<strong
><span class="function_modname">awesome.</span>kill
<span class="function_args"> <b>(</b>pid, sig<b>)</b></span></strong
>
<span class="proptype"
><span class="summary_type"> -&gt;&nbsp;boolean</span></span
>
<span class="baseclass"> </span>
</dt>
<dd>
Send a signal to a process.
<h3>Parameters:</h3>
<table class="see_also">
<tbody>
<tr class="param_header">
<th>Name</th>
<th></th>
<th>Type(s)</th>
<th>Description</th>
</tr>
<tr>
<td><span class="parameter">pid</span></td>
<td></td>
<td>
<span class="types"><span class="type">integer</span></span>
</td>
<td class="see_also_description">
Process identifier. 0 and negative values have special meaning. See
<code>man 3 kill</code>.
</td>
</tr>
<tr>
<td><span class="parameter">sig</span></td>
<td></td>
<td>
<span class="types"><span class="type">integer</span></span>
</td>
<td class="see_also_description">
Signal number. See
<a href="../core_components/awesome.html#unix_signal"
>awesome.unix_signal</a
>
for a list of signals.
</td>
</tr>
</tbody>
</table>
<h3>Returns:</h3>
<ol>
<span class="types"><span class="type">boolean</span></span>
true if the signal was successfully sent, else false
</ol>
</dd>
</dl>
]],
"awesome", -- The module name must be the same as the module name in the doc
{
children = {
{
children = {},
name = "Signal",
token = "enum",
},
{
parameters = {
{
types = { "integer" },
name = "pid",
token = "variable",
},
{
types = { "integer" },
name = "sig",
token = "variable",
},
},
return_types = { "boolean" },
name = "kill",
token = "function",
}
},
name = "Awesome",
module_path = "awesome",
dependencies = {},
token = "module",
}))
it("should produce a Record node when a function parameter is a named-parameter-table", test(
[[
<h2 class="section-header">
<a name="Static_module_functions"></a>Static module functions
</h2>
<dl class="function">
<dt>
<a
class="copy-link js-copy-link"
name="awful.screen.focused"
href="#awful.screen.focused"
>🔗</a
>
<strong
>awful.screen.focused
<span class="function_named_args"><b>{</b>[args]<b>}</b></span></strong
>
<span class="proptype"
><span class="summary_type"> -&gt;&nbsp;nil <i>or</i> screen</span></span
>
<span class="baseclass"> </span>
</dt>
<dd>
<h3>Parameters:</h3>
<table class="see_also">
<tbody>
<tr class="param_header">
<th>Name</th>
<th></th>
<th>Type(s)</th>
<th>Description</th>
<th>Default value</th>
</tr>
<tr>
<td><span class="parameter">args</span></td>
<td><span class="chips">Optional</span></td>
<td>
<span class="types"><span class="type">table</span></span>
</td>
<td class="see_also_description"></td>
<td><span class="not_applicable">Undefined</span></td>
</tr>
<tr class="see_also_sublist">
<td><span class="parameter">client</span></td>
<td><span class="chips">Optional</span></td>
<td>
<span class="types"><span class="type">boolean</span></span>
</td>
<td class="see_also_description">
Use the client screen instead of the mouse screen.
</td>
<td>
<span class="default_value"><code>false</code></span>
</td>
</tr>
<tr class="see_also_sublist">
<td><span class="parameter">mouse</span></td>
<td><span class="chips">Optional</span></td>
<td>
<span class="types"><span class="type">boolean</span></span>
</td>
<td class="see_also_description">Use the mouse screen</td>
<td>
<span class="default_value"><code>true</code></span>
</td>
</tr>
</tbody>
</table>
<h3>Returns:</h3>
<ol>
<span class="types"
>optional
<a class="type" href="../core_components/screen.html#screen"
>screen</a
></span
>
The focused screen object, or
<code>nil</code>
in case no screen is present currently.
</ol>
</dd>
</dl>
]],
"awful.screen",
{
children = {
{
children = {},
name = "Signal",
token = "enum",
},
{
children = {
{
types = { "boolean" },
name = "client",
token = "variable",
},
{
types = { "boolean" },
name = "mouse",
token = "variable",
}
},
name = "Focused_Args",
token = "record",
},
{
parameters = {
{
types = { "Focused_Args" },
name = "args",
token = "variable",
},
},
return_types = { "screen" },
name = "focused",
token = "function",
},
},
name = "Screen",
module_path = "awful.screen",
dependencies = {},
token = "module",
}))
it("should go back to a table typed parameter when the record is empty", test(
[[
<h2 class="section-header "><a name="Static_module_functions"></a>Static module functions</h2>
<dl class="function">
<dt>
<a class="copy-link js-copy-link" name="crush" href="#crush">🔗</a>
<strong><span class="function_modname">gears.table.</span>crush <span class="function_args"> <b>(</b>target, source, <span class="optional_param">raw</span><b>)</b></span></strong>
<span class="proptype"><span class="summary_type"> -&gt;&nbsp;table</span></span>
<span class="baseclass">
</span>
</dt>
<dd>
</p><h3>Parameters:</h3>
<table class="see_also">
<tbody><tr class="param_header">
<th>Name</th>
<th></th>
<th>Type(s)</th>
<th>Description</th>
<th>Default value</th>
</tr>
<tr>
<td><span class="parameter">target</span></td>
<td></td>
<td><span class="types"><a class="type" href="https://www.lua.org/manual/5.3/manual.html#6.6" target="_blank">table</a></span></td>
<td class="see_also_description"> The target table. Values from <code>source</code> will be copied
into this table.</td>
<td><span class="not_applicable" title="This parameter is mandatory">Not applicable</span></td>
</tr>
<tr>
<td><span class="parameter">source</span></td>
<td></td>
<td><span class="types"><a class="type" href="https://www.lua.org/manual/5.3/manual.html#6.6" target="_blank">table</a></span></td>
<td class="see_also_description"> The source table. Its values will be copied into
<code>target</code>.</td>
<td><span class="not_applicable" title="This parameter is mandatory">Not applicable</span></td>
</tr>
<tr>
<td><span class="parameter">raw</span></td>
<td><span class="chips">Optional</span></td>
<td><span class="types"><span class="type">bool</span></span></td>
<td class="see_also_description"> If <code>true</code>, values will be assigned with <a href="https://www.lua.org/manual/5.3/manual.html#pdf-rawset" target="_blank">rawset</a>.
This will bypass metamethods on <code>target</code>.</td>
<td><span class="default_value"><code>false</code></span></td>
</tr>
</tbody></table>
<h3>Returns:</h3>
<ol>
<span class="types"><a class="type" href="https://www.lua.org/manual/5.3/manual.html#6.6" target="_blank">table</a></span>
The target table.
</ol>
</dd>
</dl>
]],
"gears.table",
{
children = {
{
children = {},
name = "Signal",
token = "enum",
},
{
parameters = {
{
types = { "table" },
name = "target",
token = "variable",
},
{
types = { "table" },
name = "source",
token = "variable",
},
{
types = { "bool" },
name = "raw",
token = "variable",
}
},
return_types = { "table" },
name = "crush",
token = "function",
}
},
name = "Table",
module_path = "gears.table",
dependencies = {},
token = "module",
}))
it("should go back to a table typed parameter when the record is empty and it's the last parameter", test(
[[
<h2 class="section-header"><a name="Object_methods"></a>Object methods</h2>
<dl class="function">
<dt>
<a class="copy-link js-copy-link" name="tags" href="#tags">🔗</a>
<strong
>:tags
<span class="function_args"> <b>(</b>tags_table<b>)</b></span></strong
>
<span class="proptype"
><span class="summary_type"> -&gt;&nbsp;table</span></span
>
<span class="baseclass"> · 1 signal </span>
</dt>
<dd>
<h3>Parameters:</h3>
<table class="see_also">
<tbody>
<tr class="param_header">
<th>Name</th>
<th></th>
<th>Type(s)</th>
<th>Description</th>
</tr>
<tr>
<td><span class="parameter">tags_table</span></td>
<td></td>
<td>
<span class="types"
><a
class="type"
href="https://www.lua.org/manual/5.3/manual.html#6.6"
target="_blank"
>table</a
></span
>
</td>
<td class="see_also_description">
A table with tags to set, or <code>nil</code> to get the current
tags.
</td>
</tr>
</tbody>
</table>
<h3>Returns:</h3>
<ol>
<span class="types"
><a
class="type"
href="https://www.lua.org/manual/5.3/manual.html#6.6"
target="_blank"
>table</a
></span
>
A table with all tags.
</ol>
</dd>
</dl>
]],
"awful.client",
{
children = {
{
children = {},
name = "Signal",
token = "enum",
},
{
parameters = {
{
types = { "Client" },
name = "self",
token = "variable",
},
{
types = { "table" },
name = "tags_table",
token = "variable",
},
},
return_types = { "table" },
name = "tags",
token = "function",
},
},
name = "Client",
module_path = "awful.client",
dependencies = {},
token = "module",
}))
it("should return Function nodes with the `other_nodes` list when the function module name doesn't match the module name", test(
[[
<h2 class="section-header">
<a name="Static_module_functions"></a>Static module functions
</h2>
<dl class="function">
<dt>
<a class="copy-link js-copy-link" name="instances" href="#instances">🔗</a>
<strong
><span class="function_modname">client.</span>instances
<span class="function_args"> <b>(</b><b>)</b></span></strong
>
<span class="proptype"
><span class="summary_type"> -&gt;&nbsp;integer</span></span
>
<span class="baseclass"> </span>
</dt>
<dd>
Get the number of instances.
<h3>Returns:</h3>
<ol>
<span class="types"><span class="type">integer</span></span>
The number of client objects alive.
</ol>
<span id="item826" class="hide_extra"> </span>
</dd>
</dl>
]],
"awful.client",
{
children = {
{
children = {},
name = "Signal",
token = "enum",
},
},
name = "Client",
module_path = "awful.client",
dependencies = {},
token = "module",
},
{
{
parameters = {},
return_types = { "integer" },
name = "client.instances",
token = "function",
}
}))
end)

81
src/awesomewm.d.tl/ast.tl Normal file
View File

@ -0,0 +1,81 @@
local type Node = require("types.Node")
local basic_nodes <total>: { Node.Token : function(name: string, ...: any): Node } = {
module = function(name: string, module_path: string): Node
return {
token = "module",
name = name,
module_path = module_path,
dependencies = {},
children = {},
}
end,
record = function(name: string): Node
return {
token = "record",
name = name,
children = {},
}
end,
enum = function(name: string): Node
return {
token = "enum",
name = name,
children = {},
}
end,
identifier = function(name: string): Node
return {
token = "identifier",
name = name,
}
end,
variable = function(name: string): Node
return {
token = "variable",
name = name,
types = {},
}
end,
["function"] = function(name: string): Node
return {
token = "function",
name = name,
parameters = {},
return_types = {},
}
end,
metamethod = function(name: string): Node
return {
token = "metamethod",
name = name,
parameters = {},
return_types = {},
}
end,
}
local function create_node(token: Node.Token, name: string, ...: any): Node
local node = basic_nodes[token](name, ...)
return node
end
local function iter_children(node: Node): function(): integer, Node
if node.children == nil then
return function(): integer, Node end
end
return ipairs(node.children)
end
local function in_order_visitor(node: Node, visitor: function(Node))
for _, child in iter_children(node) do
in_order_visitor(child, visitor)
end
visitor(node)
end
return {
create_node = create_node,
iter_children = iter_children,
in_order_visitor = in_order_visitor,
}

30
src/awesomewm.d.tl/dag.tl Normal file
View File

@ -0,0 +1,30 @@
local type Dag = require("types.Dag")
local type Node = require("types.Node")
local utils = require("utils")
local function new(): Dag
local dag <total>: Dag = {
modules = {},
global_nodes = {},
}
return dag
end
local function push_module(dag: Dag, module_path: string, root_node: Node)
dag.modules[module_path] = root_node
end
local function push_global_nodes(dag: Dag, nodes: { Node })
utils.spread(dag.global_nodes, nodes)
end
local function iter_modules(dag: Dag): function(): string, Node
return pairs(dag.modules)
end
return {
new = new,
push_module = push_module,
push_global_nodes = push_global_nodes,
iter_modules = iter_modules,
}

View File

@ -1,6 +0,0 @@
return {
global_env_def = require "generator.global_env_def",
module_init_definition = require "generator.module_init_definition",
snippets = require "generator.snippets",
teal_type_definitions = require "generator.teal_type_definitions",
}

View File

@ -1,56 +0,0 @@
local Module_Doc = require "entity.Module_Doc"
local template = require "pl.template"
local utils = require "utils"
local snippets = require "generator.snippets"
-- The long therm goal is to have so many `snippets.render_*` functions that
-- we can render the whole file with the smallest template possible.
local tmpl = [[
-- Auto generated file (Do not manually edit this file!)
# if module.requires:len() ~= 0 then
$(snippets.render_requires(module.requires))
# end -- /requires
local record $(module.record_name)
# if #module.signals ~= 0 then
$(snippets.indent(snippets.render_enum("Signal", module.signals)))
# end -- /signals
# if #module.methods ~= 0 then
-- Object methods
$(snippets.indent(snippets.render_record_functions(module.methods)))
# end -- /methods
# if #module.properties ~= 0 then
-- Object properties
$(snippets.indent(snippets.render_record_properties(module.properties)))
# end -- /properties
# if #module.constructors ~= 0 then
-- Constructors
$(snippets.indent(snippets.render_record_functions(module.constructors)))
# end -- /constructors
# if #module.static_functions ~= 0 then
-- Static functions
$(snippets.indent(snippets.render_record_functions(module.static_functions)))
# end -- /static_functions
end
return $(module.record_name)
]]
local module = {}
function module.generate_teal(data: Module_Doc.Module_Doc): string
local tmpl_args = {
ipairs = ipairs,
module = data,
snippets = snippets,
}
return utils.do_or_fail(template.substitute, tmpl, tmpl_args)
end
return module

View File

@ -1,14 +1,22 @@
if os.getenv("LOCAL_LUA_DEBUGGER_VSCODE") == "1" then
require("lldebugger").start()
end
local ast = require "ast"
local crawler = require "crawler"
local dag = require "dag"
local filesystem = require "filesystem"
local generator = require "generator"
local printer = require "printer"
local List = require "pl.List"
local logger = require "logger"
local Map = require "pl.Map"
local Module_Doc = require "entity.Module_Doc"
local Module_Info = require "entity.Module_Info"
local module_dependencies = require "visitors.module_dependencies"
local type Node = require "types.Node"
local property = require "property"
local scraper = require "scraper"
local stringx = require "pl.stringx"
local type_mapping = require "visitors.type_mapping"
local utils = require "utils"
local log = logger.log("main")
@ -38,7 +46,7 @@ local function module_lists(
end
-- The module's children list produced can contain duplicates.
-- We ignore them for now because they are dismissed when building a Map for the generator.
-- We ignore them for now because they are dismissed when building a Map for the printer.
local function modules_tree(modules: List<Module_Info.Module_Info>): Map<string, List<string>>
local tree: Map<string, List<string>> = Map()
for module in modules:iter() do
@ -65,27 +73,37 @@ local function modules_tree(modules: List<Module_Info.Module_Info>): Map<string,
return tree
end
local function do_one_file(
url: string,
record_name: string,
output_path: string
--- TODO : rewrite this to use the DAG
local function do_module_init_definition(
module_infos: List<Module_Info.Module_Info>
)
local html = crawler.fetch(url)
local module_doc = scraper.module_doc.get_doc_from_page(html, record_name)
module_doc:fixup()
module_doc:populate_requires()
local tree = modules_tree(module_infos)
for module, children in tree:iter() do
-- TODO : this map should be coupled with the all_module_infos list
local requires: Map<string, string> = Map()
for child in children:iter() do
local name = child:gmatch(".*%.(.*)$")()
requires:set(name, child)
end
filesystem.file_writer.write(
generator.teal_type_definitions.generate_teal(module_doc),
output_path
printer.module_init_definition.generate_teal(requires),
property.out_directory .. "/" .. stringx.split(module, "."):slice(1, -1):join("/") .. "/init.d.tl"
)
end
end
--- TODO : rewrite the module_info thingy
local all_module_infos, module_infos, global_module_infos = module_lists(
property.base_url .. property.index_uri,
List(property.capi_modules),
List(property.ignored_modules)
List(),-- List(property.capi_modules),
List(utils.spread(
property.ignored_modules,
{
-- Modules that broke the parser.
--- TODO : fix the parser
"awful.screenshot",
}))
)
local tree = modules_tree(module_infos)
log:info(
logger.message_with_metadata(
@ -94,49 +112,52 @@ log:info(
total_module_count = #all_module_infos,
module_count = #module_infos,
global_module_count = #global_module_infos,
tree_items = tree:len(),
}
)
)
-- Build the DAG
local module_dag = dag.new()
for module in module_infos:iter() do
do_one_file(
property.base_url .. "/" .. module.uri,
module.name,
property.out_directory .. "/" .. module.name:gsub("%.", "/") .. ".d.tl"
local module_ast, other_nodes = scraper.module_doc.get_doc_from_page(
crawler.fetch(property.base_url .. "/" .. module.uri),
(module.name:gsub(".*%sand%s", ""))
)
dag.push_module(module_dag, module_ast.module_path, module_ast)
dag.push_global_nodes(module_dag, other_nodes)
end
local global_env_def: List<Module_Doc.Module_Doc> = List()
for module in global_module_infos:iter() do
if module.name:gmatch(".*%sand%s.*") then
do_one_file(
property.base_url .. "/" .. module.uri,
module.name,
property.out_directory .. "/" .. module.name:gsub(".*%sand%s", ""):gsub("%.", "/") .. ".d.tl"
)
end
local html = crawler.fetch(property.base_url .. "/" .. module.uri)
local module_doc = scraper.module_doc.get_doc_from_page(html, (module.name:gsub("%sand%s.*", "")))
module_doc:fixup()
module_doc.record_name = utils.lowercase(module_doc.record_name)
global_env_def:append(module_doc)
-- Run the visitors
for _,root in dag.iter_modules(module_dag) do
ast.in_order_visitor(root, function(node: Node)
type_mapping.visit(node)
end)
ast.in_order_visitor(root, function(node: Node)
module_dependencies.visit(node, root, module_dag)
end)
end
filesystem.file_writer.write(
generator.global_env_def.generate_teal(global_env_def),
property.out_directory .. "/global_env.d.tl"
)
for module, children in tree:iter() do
-- TODO : this map should be coupled with the all_module_infos list
local requires: Map<string, string> = Map()
for child in children:iter() do
local name = child:gmatch(".*%.(.*)$")()
requires:set(name, child)
end
-- Build the global module from dag.global_nodes
--- TODO : todo
--- TODO : this is fun, but we need to do something with it
-- Write the DAG to a file
-- local inspect = require("inspect")
-- filesystem.file_writer.write(
-- inspect(module_dag, { newline = "\n", indent = " ", depth = 2 }),
-- "generated_dag.lua"
-- )
log:info("Preprocessing finished")
-- Write modules types definitions to files
for module_path, root in dag.iter_modules(module_dag) do
filesystem.file_writer.write(
generator.module_init_definition.generate_teal(requires),
property.out_directory .. "/" .. stringx.split(module, "."):slice(1, -1):join("/") .. "/init.d.tl"
printer.teal_type_definition.printer(root),
property.out_directory .. "/" .. module_path:gsub("%.", "/") .. ".d.tl"
)
end
do_module_init_definition(module_infos)
log:info("Module init files generated")

View File

@ -2,7 +2,7 @@ local List = require "pl.List"
local Module_Doc = require "entity.Module_Doc"
local template = require "pl.template"
local utils = require "utils"
local snippets = require "generator.snippets"
local snippets = require "printer.snippets"
-- The long therm goal is to have so many `snippets.render_*` functions that
-- we can render the whole file with the smallest template possible.

View File

@ -0,0 +1,6 @@
return {
global_env_def = require("printer.global_env_def"),
module_init_definition = require("printer.module_init_definition"),
snippets = require("printer.snippets"),
teal_type_definition = require("printer.teal_type_definition"),
}

View File

@ -1,7 +1,7 @@
local Map = require "pl.Map"
local template = require "pl.template"
local utils = require "utils"
local snippets = require "generator.snippets"
local snippets = require "printer.snippets"
-- The long therm goal is to have so many `snippets.render_*` functions that
-- we can render the whole file with the smallest template possible.

View File

@ -0,0 +1,167 @@
local ast = require("ast")
local logger = require("logger")
local type Node = require("types.Node")
local stringx = require("pl.stringx")
local utils = require("utils")
local log = logger.log("scraper")
local function render_types(types: { string }): string
if not types or #types == 0 then
return ""
end
return ": " .. table.concat(types, " | ")
end
local function dedent(str: string): string
return stringx.dedent(str):sub(1, -2)
end
local function render_code(code: string, indent_level: integer): string
if not code or code == "" then
return ""
end
local generated = ""
for line in stringx.lines(dedent(code)) do
generated = generated .. stringx.rstrip(string.rep(" ", 3 * indent_level) .. line) .. "\n"
end
return generated
end
local function render_require(dependencies: { string : string }): string
local generated = ""
for dependency, path in utils.pairsByKeys(dependencies) do
generated = generated .. string.format("local type %s = require(\"%s\")\n", dependency, path)
end
return generated
end
local record Node_Printer_Function
on_node: function(node: Node, indent_level: integer): string, integer
before_node: nil | function(node: Node, indent_level: integer): string, integer
after_node: nil | function(node: Node, indent_level: integer): string, integer
end
-- pre-declare functions to prevent forward reference errors
local print_teal: function(node: Node, indent_level: integer | nil): string
local print_children: function(node: Node): string
local node_printer <total>: { Node.Token : Node_Printer_Function } = {
["module"] = {
before_node = function(node: Node, indent_level: integer): string, integer
return render_code(
string.format(
"-- This file was auto-generated.\n%s\nlocal record %s",
render_require(node.dependencies), -- last require statement will have a newline
node.name),
indent_level), indent_level + 1
end,
on_node = function(node: Node, indent_level: integer): string, integer
return render_code(print_children(node), indent_level), indent_level
end,
after_node = function(node: Node, indent_level: integer): string, integer
return render_code("end", indent_level - 1) ..
"\n" ..
render_code(
string.format("return %s", node.name),
indent_level - 1
), indent_level - 1
end,
},
["record"] = {
before_node = function(node: Node, indent_level: integer): string, integer
return render_code(
string.format(
"record %s",
node.name),
indent_level), indent_level + 1
end,
on_node = function(node: Node, indent_level: integer): string, integer
return render_code(print_children(node), indent_level), indent_level
end,
after_node = function(_: Node, indent_level: integer): string, integer
return render_code("end", indent_level - 1), indent_level - 1
end,
},
["enum"] = {
before_node = function(node: Node, indent_level: integer): string, integer
return render_code(
string.format(
"enum %s",
node.name),
indent_level), indent_level + 1
end,
on_node = function(node: Node, indent_level: integer): string, integer
return render_code(print_children(node), indent_level), indent_level
end,
after_node = function(_: Node, indent_level: integer): string, integer
return render_code("end", indent_level - 1), indent_level - 1
end,
},
["identifier"] = {
on_node = function(node: Node, indent_level: integer): string, integer
return render_code(string.format('"%s"', node.name), indent_level), indent_level
end,
},
["variable"] = {
on_node = function(node: Node, indent_level: integer): string, integer
return render_code(
string.format(
"%s%s",
node.name,
render_types(node.types)),
indent_level), indent_level
end,
},
["function"] = {
on_node = function(node: Node, indent_level: integer): string, integer
local args = {}
for _, parameter in ipairs(node.parameters) do
table.insert(args, print_teal(parameter):sub(1, -2)) -- need to remove the newline ending
end
return render_code(
string.format(
"%s: function(%s)%s",
node.name,
table.concat(args, ", "),
render_types(node.return_types)),
indent_level), indent_level
end,
},
["metamethod"] = {
on_node = function(): string, integer
log:warn("Metamethods are not supported yet")
end,
}
}
function print_teal(node: Node, indent_level: integer | nil): string
indent_level = indent_level or 0
local printer = node_printer[node.token]
local generated = ""
local full_generated = ""
if printer.before_node then
generated, indent_level = (printer.before_node as function(Node, integer): string, integer)(node, indent_level)
full_generated = generated
end
generated, indent_level = printer.on_node(node, indent_level)
full_generated = full_generated .. generated
if printer.after_node then
generated, indent_level = (printer.after_node as function(Node, integer): string, integer)(node, indent_level)
full_generated = full_generated .. generated
end
return full_generated
end
function print_children(node: Node): string
local generated = ""
for _, child in ast.iter_children(node) do
generated = generated .. print_teal(child)
end
return generated
end
return {
printer = print_teal,
}

View File

@ -1,14 +1,10 @@
local Function_Info = require "entity.Function_Info"
local List = require "pl.List"
local ast <const> = require("ast")
local type Node = require("types.Node")
local logger = require "logger"
local Map = require "pl.Map"
local Module_Doc = require "entity.Module_Doc"
local scan = require "web_sanitize.query.scan_html"
local scraper_utils = require "scraper.utils"
local stringx = require "pl.stringx"
local Type_Info = require "entity.Type_Info"
local utils = require "utils"
local Variable_Info = require "entity.Variable_Info"
local log = logger.log("scraper")
@ -16,176 +12,184 @@ local function extract_node_text(node: scan.HTMLNode): string
return utils.sanitize_string(node:inner_text())
end
local function parse_parameter_types(parameter_type: string): List<Type_Info.Type_Info>
local function parse_parameter_types(parameter_type: string): { string }
if parameter_type == "" then
local type_info: Type_Info.Type_Info = Type_Info("any")
return List({ type_info })
return { "any" }
end
return stringx.split(parameter_type, " or "):map(
function(type_name: string): Type_Info.Type_Info
return Type_Info(utils.sanitize_string(type_name))
local types = {}
for t in stringx.split(parameter_type, " or "):iter() do
table.insert(types, t)
end
)
return types
end
local function extract_item_name(item_name_node: scan.HTMLNode): string
return item_name_node and ((item_name_node.attr.name as string):gsub("^.*[%.:]", ""))
local function extract_item_name(item_name_node: scan.HTMLNode): string, string | nil
if not item_name_node then
return
end
local module_name_node <const> = scraper_utils.find(item_name_node:outer_html(), "span.function_modname")[1]
local module_name = module_name_node and module_name_node:inner_text():gsub("[%.:]$", "")
local name <const> = item_name_node:inner_text():gsub("^.*[%.:](.+)%s*[%(%{].*[%)%}]", "%1")
return utils.sanitize_string(name), module_name and utils.sanitize_string(module_name) or nil
end
local function extract_function_parameter_Parameters(tr_node: scan.HTMLNode): { Variable_Info.Variable_Info }
local query_selectors = {
name = "span.parameter",
types = "span.types"
}
return scraper_utils.scrape_tuples(
tr_node:outer_html(),
{ query_selectors.name, query_selectors.types },
function(nodes: { string : scan.HTMLNode | nil }): Variable_Info.Variable_Info
return Variable_Info(
extract_node_text(nodes[query_selectors.name] as scan.HTMLNode),
parse_parameter_types(extract_node_text(nodes[query_selectors.types] as scan.HTMLNode))
)
end)
end
local function extract_function_parameters(function_parameters_node: scan.HTMLNode): { Variable_Info.Variable_Info }
local current_record_parameter: Type_Info.Type_Info | nil = nil
return scraper_utils.scrape(
function_parameters_node:outer_html(),
"tr",
function(line_node: scan.HTMLNode): Variable_Info.Variable_Info
local parameters = extract_function_parameter_Parameters(line_node)
if #parameters == 0 then
return nil
elseif #parameters ~= 1 then
log:error(logger.message_with_metadata("Expected 1 parameter by <tr> node",
{ len = #parameters, line_node = line_node, parameters = parameters }))
error("Expected 1 parameter by <tr> node")
end
local name, types = parameters[1].name, parameters[1].types
if line_node.attr ~= nil and line_node.attr.class == "see_also_sublist" and current_record_parameter then
local record_parameter = current_record_parameter as Type_Info.Type_Info
if not record_parameter.record_entries then
record_parameter.record_entries = Map()
end
(record_parameter.record_entries as Map<string, List<Type_Info.Type_Info>>):set(name, types)
local function extract_function_parameters(table_html: string, function_name: string): { Node }, { Node }
local parameters_types <const>: { Node } = {}
local is_populating_parameters_types: boolean = false
local current_field: Node = nil
local parameters <const> = scraper_utils.scrape(table_html, "tr", function(tr: scan.HTMLNode): Node
local tr_html = tr:outer_html()
local name_node <const> = scraper_utils.find(tr_html, "span.parameter")[1]
local types_node <const> = scraper_utils.find(tr_html, "span.types")[1]
if not name_node or not types_node then
return nil
end
if #types == 1 and types[1].name == "table" then
local record_name = utils.capitalize(name)
current_record_parameter = Type_Info(record_name)
return Variable_Info(
name,
List({ current_record_parameter })
)
local name <const> = extract_node_text(name_node)
local types <const> = parse_parameter_types(extract_node_text(types_node))
-- Add a field to the current parameter type record
if tr.attr ~= nil and tr.attr.class == "see_also_sublist" and is_populating_parameters_types then
local field = ast.create_node("variable", name)
field.types = types
table.insert(parameters_types[#parameters_types].children, field)
return nil
end
return Variable_Info(name, types)
-- Still here and we are populating the parameter type record ?
-- Then oops, we wrongly tried to convert a table to a record
if is_populating_parameters_types then
table.remove(parameters_types, #parameters_types)
is_populating_parameters_types = false
current_field.types = { "table" }
end
-- Otherwise, add a new parameter
local field = ast.create_node("variable", name)
field.types = types
current_field = field
-- If the parameter is a table, then we try to convert it to a record
if #types == 1 and types[1] == "table" then
local record_name <const> = string.format(
"%s_%s",
utils.capitalize(function_name),
utils.capitalize(name))
table.insert(parameters_types, ast.create_node("record", record_name))
is_populating_parameters_types = true
field.types = { record_name }
end
return field
end)
if is_populating_parameters_types and #parameters_types[#parameters_types].children == 0 then
table.remove(parameters_types, #parameters_types)
current_field.types = { "table" }
end
return parameters, parameters_types
end
local function extract_function_return_types(function_return_types_node: scan.HTMLNode): List<Type_Info.Type_Info>
if not function_return_types_node then
return {}
end
local selector = "span.types .type"
local html = function_return_types_node:outer_html()
return List(scraper_utils.scrape(html, selector, extract_node_text)):map(
function(type_name: string): Type_Info.Type_Info
return Type_Info(type_name)
end)
local function extract_function_return_types(ol_html: string): { string }
return scraper_utils.scrape(ol_html, "span.types .type", extract_node_text)
end
local function extract_property_constraints(property_constraint_node: scan.HTMLNode): { string }
return scraper_utils.scrape(
property_constraint_node:outer_html(),
"tr.see_also_sublist",
"tr.see_also_sublist i code",
extract_node_text
)
end
local function extract_section_functions(dl: string): { Function_Info.Function_Info }
local query_selectors = {
header = "dt",
name = "a",
local function extract_section_functions(dl: string, module_name: string | nil): { Node }, { Node}
local list_query_selectors <const>: { string : string } = {
function_name = "dt strong",
body = "dd",
parameters = "table",
return_types = "ol",
}
return scraper_utils.scrape_tuples(
local functions <const>: { Node } = {}
local other_functions <const>: { Node } = {}
for nodes in scraper_utils.iter_tuples(
dl,
{ query_selectors.header, query_selectors.body },
function(nodes: { string : scan.HTMLNode | nil }): Function_Info.Function_Info
if not nodes[query_selectors.header] or not nodes[query_selectors.body] then
log:warn(
logger.message_with_metadata(
"Missing header or body",
{ nodes = nodes }
utils.values(list_query_selectors)
) do
local function_name <const>, function_module_name <const> = extract_item_name(nodes[list_query_selectors.function_name])
local function_node <const> = ast.create_node(
"function",
function_name
)
)
error("Missing header or body")
local body_html = nodes[list_query_selectors.body]:outer_html()
local parameter_node = scraper_utils.find(body_html, "table")[1]
local parameters, parameters_types: { Node }, { Node } = {}, {}
if parameter_node then
parameters, parameters_types = extract_function_parameters(parameter_node:outer_html(), function_name)
end
local header = nodes[query_selectors.header] as scan.HTMLNode
local body = nodes[query_selectors.body] as scan.HTMLNode
local body_elements = scraper_utils.extract_nodes(
body:outer_html(),
{ query_selectors.parameters, query_selectors.return_types }
)
return Function_Info(
scraper_utils.scrape(
header:outer_html(),
query_selectors.name,
extract_item_name
)[1],
#body_elements:get(query_selectors.parameters) ~= 0 and
List(extract_function_parameters(body_elements:get(query_selectors.parameters)[1])) or
(List() as List<Variable_Info.Variable_Info>),
#body_elements:get(query_selectors.return_types) ~= 0 and
extract_function_return_types(body_elements:get(query_selectors.return_types)[1]) or
(List() as List<Type_Info.Type_Info>)
)
function_node.parameters = parameters
local return_node = scraper_utils.find(body_html, "ol")[1]
function_node.return_types = return_node and
extract_function_return_types(return_node:outer_html()) or
{}
if module_name and function_module_name and module_name ~= function_module_name then
function_node.name = function_module_name .. "." .. function_node.name
utils.spread(other_functions, parameters_types)
table.insert(other_functions, function_node)
else
utils.spread(functions, parameters_types)
table.insert(functions, function_node)
end
)
end
return functions, other_functions
end
local function extract_section_variables(dl: string): { Variable_Info.Variable_Info }
local query_selectors = {
variable_name = "dt a",
local function extract_section_variables(dl: string): { Node }, { string }
local query_selectors <const>: { string : string } = {
variable_name = "dt strong",
variable_summary_type = "dt span.summary_type",
variable_property_constraint = "dd span.property_type",
}
return scraper_utils.scrape_tuples(
local variables <const>: { Node } = {}
local signals <const>: { string } = {}
for nodes in scraper_utils.iter_tuples(
dl,
{ query_selectors.variable_name, query_selectors.variable_summary_type, query_selectors.variable_property_constraint },
function(nodes: { string : scan.HTMLNode | nil }): Variable_Info.Variable_Info
local variable_info = Variable_Info()
utils.values(query_selectors)
) do
local node = ast.create_node("variable", (extract_item_name(nodes[query_selectors.variable_name])))
node.types = parse_parameter_types(extract_node_text(nodes[query_selectors.variable_summary_type]))
variable_info.name = extract_item_name(nodes[query_selectors.variable_name])
variable_info.types = parse_parameter_types(extract_node_text(nodes[query_selectors.variable_summary_type]))
if #variable_info.types == 1 and variable_info.types[1].name == "string" then
log:debug("extract variable string with constraints, this is an enum")
variable_info.constraints = List(extract_property_constraints(nodes[query_selectors.variable_property_constraint])):map(
function(constraint: string): string
return (constraint:gsub("&quot;", ""))
end
if #node.types == 1 and node.types[1] == "string" then
log:debug("extract variable string with constraints, this is an enum", { name = node.name })
local type_enum <const> = ast.create_node("enum", utils.capitalize(node.name))
for _, constraint in ipairs(extract_property_constraints(nodes[query_selectors.variable_property_constraint])) do
table.insert(
type_enum.children,
ast.create_node("identifier", (constraint:gsub("&quot;", "")))
)
end
return variable_info
if #type_enum.children == 0 then
log:debug("Enum has no children, get back to variable", { name = node.name })
else
table.insert(variables, type_enum)
node.types = { type_enum.name }
end
)
end
table.insert(variables, node)
table.insert(signals, string.format("property::%s", node.name)) -- TODO : actually scrape the signals from the doc
end
return variables, signals
end
local function extract_section_signal(dl: string): { string }
@ -202,58 +206,93 @@ local enum Section
"Signals"
end
local section_scrapers: { Section : function(html: string, module_doc: Module_Doc.Module_Doc) } = {
["Constructors"] = function(html: string, module_doc: Module_Doc.Module_Doc)
module_doc.constructors = List(extract_section_functions(html))
end,
["Static module functions"] = function(html: string, module_doc: Module_Doc.Module_Doc)
module_doc.static_functions = List(extract_section_functions(html))
end,
["Object properties"] = function(html: string, module_doc: Module_Doc.Module_Doc)
module_doc.properties = List(extract_section_variables(html))
end,
["Object methods"] = function(html: string, module_doc: Module_Doc.Module_Doc)
local self_parameter = Variable_Info("self", List({ Type_Info(module_doc.record_name) }))
module_doc.methods = List(extract_section_functions(html)):map(
function(method: Function_Info.Function_Info): Function_Info.Function_Info
method.parameters:insert(1, self_parameter)
return method
-- returns
-- - Nodes that should be added to the module
-- - Nodes that should be added to the global scope
-- - Strings that should be added to the record Signals
local section_scrapers <total>: { Section : function(html: string, record_name: string, module_name: string): { Node }, { Node }, { string } } = {
["Constructors"] = function(html: string): { Node }, { Node }, { string }
local constructors <const> = extract_section_functions(html)
for _, constructor in ipairs(constructors) do
if constructor.token == "function" then
constructor.name = "new"
end
)
end
return constructors, {}, {}
end,
["Signals"] = function(html: string, module_doc: Module_Doc.Module_Doc)
module_doc.signals = List(extract_section_signal(html))
["Static module functions"] = function(html: string, _: string, module_name: string): { Node }, { Node }, { string }
local static_functions, other_functions = extract_section_functions(html, module_name)
return static_functions, other_functions, {}
end,
["Object properties"] = function(html: string): { Node }, { Node }, { string }
local properties, signals = extract_section_variables(html)
return properties, {}, signals
end,
["Object methods"] = function(html: string, record_name: string): { Node }, { Node }, { string }
local methods <const> = extract_section_functions(html)
for _, method in ipairs(methods) do
if method.token == "function" then
local self_parameter = ast.create_node("variable", "self")
self_parameter.types = { record_name }
table.insert(method.parameters, 1, self_parameter)
end
end
return methods, {}, {}
end,
["Signals"] = function(html: string): { Node }, { Node }, { string }
local signals = extract_section_signal(html)
return {}, {}, signals
end,
}
-- local function extract_node_module_name(node: Node): string
-- return (node.name:gsub("(.*)[%.:].+$", "%1"))
-- end
local module = {}
function module.get_doc_from_page(html: string, module_name: string): Module_Doc.Module_Doc
local nodes = scraper_utils.extract_nodes(html, {
function module.get_doc_from_page(html: string, module_path: string): Node, { Node }
local html_nodes = scraper_utils.extract_nodes(html, {
"h2.section-header",
"dl.function",
})
if #nodes:get "h2.section-header" ~= #nodes:get "dl.function" then
if #html_nodes:get "h2.section-header" ~= #html_nodes:get "dl.function" then
error "The list aren't the same size!"
end
local module_doc = Module_Doc()
module_doc.record_name = utils.capitalize((module_name:gsub(".*%.", "")))
local record_name <const> = utils.capitalize((module_path:gsub(".*%.", "")))
local module_root <const> = ast.create_node("module", record_name, module_path)
local other_nodes <const>: { Node } = {}
for i = 1, #nodes:get("h2.section-header") do
local h2 = nodes:get("h2.section-header")[i]
local module_signals_node <const> = ast.create_node("enum", "Signal")
table.insert(module_root.children, module_signals_node)
for i = 1, #html_nodes:get("h2.section-header") do
local h2 = html_nodes:get("h2.section-header")[i]
local section_name = utils.sanitize_string(h2:inner_text()) as Section -- promote to Section, we then test if the section_name is in the table
local dl_html = nodes:get("dl.function")[i]:outer_html()
local dl_html = html_nodes:get("dl.function")[i]:outer_html()
if section_scrapers[section_name] then
section_scrapers[section_name](dl_html, module_doc)
local module_nodes, global_nodes, signals_name = section_scrapers[section_name](dl_html, record_name, module_path)
for _, node in ipairs(module_nodes) do
table.insert(module_root.children, node)
end
for _, node in ipairs(global_nodes) do
table.insert(other_nodes, node)
end
for _, signal_name in ipairs(signals_name) do
table.insert(
module_signals_node.children,
ast.create_node("identifier", signal_name)
)
end
else
log:warn("Section scraper not implemented: " .. section_name)
end
end
return module_doc
return module_root, other_nodes
end
return module

View File

@ -15,15 +15,14 @@ function scraper_utils.scrape<T>(html: string, query_selector: string, extract_c
scanner.scan_html(html, function(stack: scan.NodeStack)
if stack:is(query_selector) then
local node = stack:current()
local success, info_or_error = pcall(extract_callback, node)
local success, ret_or_error = pcall(extract_callback, node)
if not success then
local error_message = info_or_error as string
log:error(logger.message_with_metadata("Extraction error", { error = error_message }))
else
local info = info_or_error as T
table.insert(ret, info)
log:error(logger.message_with_metadata("Extraction error", { error = ret_or_error as string }))
return
end
table.insert(ret, ret_or_error as T)
end
end)
@ -48,28 +47,56 @@ function scraper_utils.extract_nodes(html: string, query_selectors: { string }):
return siblings
end
function scraper_utils.scrape_tuples<T>(html: string, query_selectors: { string }, extract_callback: function(tuple: { string : scan.HTMLNode | nil }): T): { T }
local nodes = scraper_utils.extract_nodes(html, query_selectors)
function scraper_utils.find(html: string, query_selector: string): { scan.HTMLNode }
local nodes: { scan.HTMLNode } = {}
local ret: { T } = {}
for i = 1, #nodes:get(query_selectors[1]) do
local node_list: { string : scan.HTMLNode | nil } = {}
tablex.foreach(query_selectors, function(query_selector: string)
node_list[query_selector] = nodes:get(query_selector)[i] or nil
scanner.scan_html(html, function(stack: scan.NodeStack)
if stack:is(query_selector) then
table.insert(nodes, stack:current())
end
end)
local success, info_or_error = pcall(extract_callback, node_list)
if not success then
local error_message = info_or_error as string
log:error(logger.message_with_metadata("Extraction error", { error = error_message }))
else
local info = info_or_error as T
table.insert(ret, info)
return nodes
end
function scraper_utils.iter_tuples(html: string, query_selectors: { string }): function(): { string : scan.HTMLNode }
local siblings: { string : { scan.HTMLNode } } = {}
for _, query_selector in ipairs(query_selectors) do
siblings[query_selector] = {}
end
scanner.scan_html(
html,
function(stack: scan.NodeStack)
for _, query_selector in ipairs(query_selectors) do
if stack:is(query_selector) then
table.insert(siblings[query_selector], stack:current())
end
end
end
)
local siblings_count = #siblings[query_selectors[1]]
for _, query_selector in ipairs(query_selectors) do
if #siblings[query_selector] ~= siblings_count then
error("Query selectors do not have the same number of siblings")
end
end
return ret
local i = 0
return function(): { string : scan.HTMLNode }
i = i + 1
if i > siblings_count then
return nil
end
local node_list: { string : scan.HTMLNode } = {}
for _, query_selector in ipairs(query_selectors) do
node_list[query_selector] = siblings[query_selector][i]
end
return node_list
end
end
return scraper_utils

View File

@ -0,0 +1,8 @@
local type Node = require("types.Node")
local record Dag
modules: { string : Node } -- module_path (AKA "full name" `package.module.name`) -> root_node (token = "module")
global_nodes: { Node }
end
return Dag

View File

@ -0,0 +1,29 @@
local record Node
enum Token
"module" -- file root node, it is always a record and the generated .d.tl file returns it
"record"
"enum"
"identifier" -- blank token with only a name (used for enum values)
"variable"
"function"
"metamethod"
end
token: Token
name: string
-- for "module", "record", "enum"
children: { Node }
-- for "variable"
types: { string }
-- for "function" and "metamethod"
parameters: { Node }
return_types: { string }
-- for "module"
module_path: string
dependencies: { string : string } -- module_name -> module_path
end
return Node

View File

@ -35,6 +35,16 @@ function utils.map<T, U>(list: { T }, iteratee: function(value: T, position: int
return mapped
end
function utils.values<T>(t: table): { T }
local values: { T } = {}
for _, v in pairs(t) do
table.insert(values, v as T)
end
return values
end
function utils.sanitize_string(s: string): string
return (stringx.strip(web_sanitize.extract_text(s)))
end
@ -66,4 +76,29 @@ function utils.do_or_fail<T>(func: function<T>(...: any): (T | nil, string), ...
return res as T -- promote to T since pcall succeeded at this point
end
function utils.spread<T>(t: { T }, ...: { T }): { T }
for _, a in ipairs({ ... }) do
for _, v in ipairs(a) do
table.insert(t, v)
end
end
return t
end
function utils.pairsByKeys<Key, Value>(list: { Key : Value }, comp: function(Key, Key): boolean): function(): Key, Value
local sortedKeys = {}
for n in pairs(list) do table.insert(sortedKeys, n) end
table.sort(sortedKeys, comp)
local index = 0
return function(): Key, Value
index = index + 1
if sortedKeys[index] == nil then
return nil
end
return sortedKeys[index], list[sortedKeys[index]]
end
end
return utils

View File

@ -0,0 +1,70 @@
local type Dag = require("types.Dag")
local type Node = require("types.Node")
local utils = require("utils")
local spread = utils.spread
local function get_all_types_in_node(node: Node): { string }
local parameters_types = {}
if node.parameters then
for _, v in ipairs(node.parameters) do
spread(parameters_types, v.types)
end
end
return spread(
{},
node.types or {},
node.return_types or {},
parameters_types or {})
end
local function replace_in_node_type(node: Node, old_type: string, new_type: string)
if node.parameters then
for _, v in ipairs(node.parameters) do
for i, t in ipairs(v.types) do
if t == old_type then
v.types[i] = new_type
end
end
end
end
if node.types then
for i, t in ipairs(node.types) do
if t == old_type then
node.types[i] = new_type
end
end
end
if node.return_types then
for i, t in ipairs(node.return_types) do
if t == old_type then
node.return_types[i] = new_type
end
end
end
end
local record Module_Dependencies
visit: function(node: Node, mod: Node, d: Dag)
end
function Module_Dependencies.visit(node: Node, mod: Node, d: Dag)
local all_types <const> = get_all_types_in_node(node)
for _, type_name in ipairs(all_types) do
if type_name == mod.name then
goto continue
end
local dependency = d.modules[type_name] or d.modules[utils.lowercase(type_name)]
if dependency then
mod.dependencies[dependency.name] = dependency.module_path
replace_in_node_type(node, dependency.module_path, dependency.name)
end
::continue::
end
end
return Module_Dependencies

View File

@ -0,0 +1,68 @@
local type Node = require("types.Node")
-- Special types I don't want to deal with for now
local gears_shape_function = "function(cr: any, width: integer, height: integer)"
local type_map <const>: { string : string } = {
awesome = "Awesome",
bool = "boolean",
client = "Client",
["gears.shape"] = gears_shape_function,
["gears.surface"] = "Surface",
image = "Image",
int = "integer",
screen = "Screen",
shape = gears_shape_function,
surface = "Surface",
tag = "Tag",
widget = "wibox.widget",
-- fixes we shouldn't have to do (We need to PR Awesome to fix the doc)
timer = "gears.timer",
}
local function get_type(t: string): string
return type_map[t] or t
end
local function check_node(node: Node)
if not node.types then
return
end
for i, t in ipairs(node.types) do
node.types[i] = get_type(t)
end
end
local function check_function_parameters(node: Node)
if not node.parameters then
return
end
for _, parameter in ipairs(node.parameters) do
check_node(parameter)
end
end
local function check_function_returns(node: Node)
if not node.return_types then
return
end
for i, ret in ipairs(node.return_types) do
node.return_types[i] = get_type(ret)
end
end
local record Type_Mapping
visit: function(node: Node)
end
function Type_Mapping.visit(node: Node)
check_node(node)
check_function_parameters(node)
check_function_returns(node)
end
return Type_Mapping

5
types/lldebugger.d.tl Normal file
View File

@ -0,0 +1,5 @@
local record Lldebugger
start: function()
end
return Lldebugger