Skip to content

Commit 4306f61

Browse files
committed
feat(keyword): add keyword module with docs, types, and tests (#6)
* feat(keyword): add keyword module with docs, types, and tests * test(keyword): fix iskeyword test name formatting * test(keyword): add isidentifier edge cases * refactor(str,utils): move identifier checks to keyword module * chore(utils): remove isidentifier from docs, types, and tests
1 parent 2448af0 commit 4306f61

14 files changed

Lines changed: 394 additions & 67 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ luarocks install mods
4848
| Module | Description |
4949
| -------------- | -------------------------------------------------------------- |
5050
| [`is`] | Type predicates for Lua values and filesystem path kinds. |
51+
| [`keyword`] | Lua keyword helpers for reserved-word checks. |
5152
| [`List`] | Python-style list helpers for mapping, filtering, and slicing. |
5253
| [`operator`] | Operator helpers as functions. |
5354
| [`Set`] | Set operations and helpers for unique values. |
@@ -72,6 +73,7 @@ Thanks to these Lua ecosystem projects:
7273
- [busted](https://github.com/lunarmodules/busted) for test framework support.
7374

7475
[`is`]: https://luamod.github.io/mods/modules/is
76+
[`keyword`]: https://luamod.github.io/mods/modules/keyword
7577
[`List`]: https://luamod.github.io/mods/modules/list
7678
[`operator`]: https://luamod.github.io/mods/modules/operator
7779
[`Set`]: https://luamod.github.io/mods/modules/set

docs/src/modules/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ description: Overview of available Mods modules and their purpose.
88
| Module | Description |
99
| ----------------------------------- | -------------------------------------------------------------- |
1010
| [`is`](/modules/is) | Type predicates for Lua values and filesystem path kinds. |
11+
| [`keyword`](/modules/keyword) | Lua keyword helpers for reserved-word checks. |
1112
| [`List`](/modules/list) | Python-style list helpers for mapping, filtering, and slicing. |
1213
| [`operator`](/modules/operator) | Operator helpers as functions. |
1314
| [`Set`](/modules/set) | Set operations and helpers for unique values. |

docs/src/modules/keyword.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
---
2+
description: Lua keyword helpers for reserved-word checks.
3+
---
4+
5+
# `keyword`
6+
7+
Lua keyword helpers.
8+
9+
## Import
10+
11+
```lua
12+
local kw = require("mods.keyword")
13+
```
14+
15+
## Dependencies
16+
17+
- [`mods.List`] is used by `kwlist()`.
18+
- [`mods.Set`] is used by `kwset()`.
19+
20+
> [!NOTE]
21+
>
22+
> These dependencies are lazy-loaded internally 💤, so requiring `mods.keyword`
23+
> does not immediately load them.
24+
25+
## Quick Reference
26+
27+
| Function | Description |
28+
| ------------------------------------------------------ | -------------------------------------------------- |
29+
| [`iskeyword(s)`](#fn-iskeywords) | Return `true` when `s` is a reserved Lua keyword. |
30+
| [`isidentifier(s)`](#fn-isidentifiers) | Return `true` for valid non-keyword identifiers. |
31+
| [`kwlist()`](#fn-kwlist) | Return Lua keywords as a [`mods.List`] of strings. |
32+
| [`kwset()`](#fn-kwset) | Return Lua keywords as a [`mods.Set`] of strings. |
33+
| [`normalize_identifier(s)`](#fn-normalize_identifiers) | Normalize input to a safe identifier. |
34+
35+
## Functions
36+
37+
### `iskeyword(s)` {#fn-iskeywords}
38+
39+
Return `true` when `s` is a reserved Lua keyword.
40+
41+
> [!NOTE]
42+
>
43+
> `goto` is treated as a keyword on Lua 5.2+ and not on Lua 5.1/LuaJIT.
44+
45+
```lua
46+
print(kw.iskeyword("function")) --> true
47+
print(kw.iskeyword("hello")) --> false
48+
```
49+
50+
### `kwlist()` {#fn-kwlist}
51+
52+
Return Lua keywords as a [`mods.List`] of strings.
53+
54+
```lua
55+
local keywords = kw.kwlist()
56+
print(keywords[1]) --> "and"
57+
print(keywords[#keywords]) --> "while"
58+
```
59+
60+
### `kwset()` {#fn-kwset}
61+
62+
Return Lua keywords as a [`mods.Set`] of strings.
63+
64+
```lua
65+
local words = kw.kwset()
66+
print(words["and"]) --> true
67+
print(words["hello"]) --> nil
68+
```
69+
70+
### `isidentifier(s)` {#fn-isidentifiers}
71+
72+
Return `true` when `s` is a valid non-keyword Lua identifier.
73+
74+
```lua
75+
print(kw.isidentifier("hello_world")) --> true
76+
print(kw.isidentifier("local")) --> false
77+
```
78+
79+
### `normalize_identifier(s)` {#fn-normalize_identifiers}
80+
81+
Normalize input to a safe Lua identifier.
82+
83+
```lua
84+
print(kw.normalize_identifier(" 2 bad-name ")) --> "_2_bad_name"
85+
print(kw.normalize_identifier("local")) --> "local_"
86+
print(kw.normalize_identifier("end")) --> "end_"
87+
print(kw.normalize_identifier(" ")) --> "_"
88+
print(kw.normalize_identifier(false)) --> "false_"
89+
```
90+
91+
[`mods.List`]: /modules/list
92+
[`mods.Set`]: /modules/set

docs/src/modules/utils.md

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,10 @@ local utils = require("mods.utils")
1616

1717
| Function | Description |
1818
| -------------------------------------- | ----------------------------------------------------- |
19-
| [`isidentifier(s)`](#fn-isidentifiers) | Checks if a string is a valid non-keyword identifier. |
2019
| [`quote(v)`](#fn-quotev) | Smart-quotes a string for readable Lua-like output. |
2120

2221
## Functions
2322

24-
### `isidentifier(s)` {#fn-isidentifiers}
25-
26-
Returns `true` when `s` is a valid Lua identifier and not a reserved keyword.
27-
28-
```lua
29-
print(utils.isidentifier("hello_world"))
30-
-- true
31-
32-
print(utils.isidentifier("local"))
33-
-- false
34-
35-
print(utils.isidentifier("2bad"))
36-
-- false
37-
```
38-
3923
### `quote(v)` {#fn-quotev}
4024

4125
Smart-quotes a string for readable Lua-like output.

mods.rockspec.template

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ description = {
1313
license = "MIT",
1414
summary = "Pure Lua modules",
1515
detailed = [[
16-
Mods provides small, focused Lua modules: List, Set, is, operator, str, stringcase, tbl, template, and validate.
16+
Mods provides small, focused Lua modules: List, Set, is, keyword, operator, str, stringcase, tbl, template, and validate.
1717
]],
1818
}
1919

@@ -26,6 +26,7 @@ build = {
2626
modules = {
2727
["mods"] = "src/mods/init.lua",
2828
["mods.is"] = "src/mods/is.lua",
29+
["mods.keyword"] = "src/mods/keyword.lua",
2930
["mods.List"] = "src/mods/List.lua",
3031
["mods.operator"] = "src/mods/operator.lua",
3132
["mods.Set"] = "src/mods/Set.lua",

spec/keyword_spec.lua

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
local List = require("mods.List")
2+
local Set = require("mods.Set")
3+
local kw = require("mods.keyword")
4+
5+
local fmt = string.format
6+
local is_lua51 = _VERSION == "Lua 5.1"
7+
8+
describe("mods.keyword", function()
9+
local fn = function() end
10+
local co = coroutine.create(fn)
11+
-- stylua: ignore
12+
local kwlist = List({
13+
"and" , "break" , "do" , "else" , "elseif",
14+
"end" , "false" , "for" , "function", "if" ,
15+
"in" , "local" , "nil" , "not" , "or" ,
16+
"repeat", "return", "then", "true" , "until" , "while"
17+
})
18+
19+
if not is_lua51 then
20+
kwlist:append("goto"):sort()
21+
end
22+
23+
local kwset = kwlist:setify()
24+
local tests
25+
26+
-------------------
27+
--- iskeyword() ---
28+
-------------------
29+
30+
local non_keywords = {
31+
"_",
32+
"",
33+
"Function",
34+
"goto1",
35+
"hello",
36+
"local_var",
37+
"nil?",
38+
"while_",
39+
{},
40+
123,
41+
false,
42+
}
43+
44+
for i = 1, #kwlist do
45+
local input = kwlist[i]
46+
it(fmt("iskeyword(%s) returns true", inspect(input)), function()
47+
assert.is_true(kw.iskeyword(input))
48+
end)
49+
end
50+
51+
for i = 1, #non_keywords do
52+
local input = non_keywords[i]
53+
it(fmt("iskeyword(%s) returns false", inspect(input)), function()
54+
assert.is_false(kw.iskeyword(input))
55+
end)
56+
end
57+
58+
----------------------
59+
--- isidentifier() ---
60+
----------------------
61+
62+
-- stylua: ignore
63+
tests = {
64+
{ "hello" , true },
65+
{ "hello_world" , true },
66+
{ "_name2" , true },
67+
{ "goto" , is_lua51 },
68+
{ "(var" , false },
69+
{ "[var" , false },
70+
{ "local" , false },
71+
{ "function" , false },
72+
{ "2bad" , false },
73+
{ "bad-name" , false },
74+
{ false , false },
75+
}
76+
77+
for i = 1, #tests do
78+
local input, expected = unpack(tests[i], 1, 2)
79+
it(fmt("isidentifier(%s)", inspect(input)), function()
80+
assert.are_equal(expected, kw.isidentifier(input))
81+
end)
82+
end
83+
84+
------------------------------
85+
--- normalize_identifier() ---
86+
------------------------------
87+
88+
-- stylua: ignore
89+
tests = {
90+
------input------|----expected----
91+
{ " 2 bad-name " , "_2_bad_name" },
92+
{ "local" , "local_" },
93+
{ "" , "_" },
94+
{ " " , "_" },
95+
{ false , "false_" },
96+
{ fn , "function_" },
97+
{ {} , "table_" },
98+
{ co , "thread_" },
99+
}
100+
101+
for i = 1, #tests do
102+
local input, expected = unpack(tests[i], 1, 2)
103+
it(fmt("normalize_identifier(%s)", inspect(input)), function()
104+
assert.are_equal(expected, kw.normalize_identifier(input))
105+
end)
106+
end
107+
108+
----------------
109+
--- kwlist() ---
110+
----------------
111+
112+
describe("kwlist()", function()
113+
it("returns all keywords in order", function()
114+
assert.are_same(kwlist, kw.kwlist())
115+
end)
116+
117+
it("returns a mods.List instance", function()
118+
local kw = kw.kwlist()
119+
assert.are_equal(List, getmetatable(kw))
120+
end)
121+
122+
it("returns a fresh copy on each call", function()
123+
local l1 = kw.kwlist()
124+
local l2 = kw.kwlist()
125+
assert.are_not_equal(l1, l2)
126+
end)
127+
end)
128+
129+
---------------
130+
--- kwset() ---
131+
---------------
132+
133+
describe("kwset()", function()
134+
it("returns all keywords", function()
135+
assert.are_same(kwset, kw.kwset())
136+
end)
137+
138+
it("returns a mods.Set instance", function()
139+
local kw = kw.kwset()
140+
assert.are_equal(Set, getmetatable(kw))
141+
end)
142+
143+
it("returns a fresh copy on each call", function()
144+
local s1 = kw.kwset()
145+
local s2 = kw.kwset()
146+
assert.are_not_equal(s1, s2)
147+
end)
148+
end)
149+
end)

spec/utils_spec.lua

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,6 @@ local utils = require("mods.utils")
55
describe("mods.utils", function()
66
local tests
77

8-
--------------------
9-
--- isidentifier ---
10-
--------------------
11-
12-
-- stylua: ignore
13-
tests = {
14-
-----input---|-expected---
15-
{ "_" , true },
16-
{ "var" , true },
17-
{ "var_2" , true },
18-
{ "2var" , false },
19-
{ "var)" , false },
20-
{ "[var" , false },
21-
{ "local" , false },
22-
{ "function" , false },
23-
{ "nil" , false },
24-
}
25-
26-
for i = 1, #tests do
27-
local input, expected = unpack(tests[i], 1, 2)
28-
it(("isidentifier(%q)"):format(input), function()
29-
assert.are_equal(expected, utils.isidentifier(input))
30-
end)
31-
end
32-
338
-------------
349
--- quote ---
3510
-------------

src/mods/init.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
local mods = {}
22

3-
("is List operator Set str stringcase tbl template utils validate"):gsub("%S+", function(name)
3+
([[is keyword List operator Set str stringcase
4+
tbl template utils validate]]):gsub("%S+", function(name)
45
mods[name] = "mods." .. name
56
end)
67

0 commit comments

Comments
 (0)