Skip to content

Commit 53dc8a3

Browse files
authored
Add ci script (#351)
1 parent 0bbd08c commit 53dc8a3

File tree

2 files changed

+304
-6
lines changed

2 files changed

+304
-6
lines changed

ci/check_all_exposed_funs_tested.roc

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br" }
2+
3+
import pf.Stdout
4+
import pf.Arg exposing [Arg]
5+
import pf.File
6+
import pf.Cmd
7+
8+
# This script performs the following tasks:
9+
# 1. Reads the file basic-cli/platform/main.roc and extracts the exposes list.
10+
# 2. For each module in the exposes list, it reads the corresponding file (e.g., basic-cli/platform/Path.roc) and extracts all functions in the module list.
11+
# 3. Checks if each module.function is used in the basic-cli/examples folder using ripgrep
12+
# 4. Prints the functions that are not used anywhere in the examples.
13+
14+
## For convenient string errors
15+
err_s = |err_msg| Err(StrErr(err_msg))
16+
17+
main! : List Arg => Result {} _
18+
main! = |_args|
19+
path_to_platform_main = "basic-cli/platform/main.roc"
20+
21+
main_content =
22+
File.read_utf8!(path_to_platform_main)?
23+
24+
exposed_modules =
25+
extract_exposes_list(main_content)?
26+
27+
# Uncomment for debugging
28+
# Stdout.line!("Found exposed modules: ${Str.join_with(exposed_modules, ", ")}")?
29+
30+
module_name_and_functions = List.map_try!(
31+
exposed_modules,
32+
|module_name|
33+
process_module!(module_name),
34+
)?
35+
36+
tagged_functions =
37+
module_name_and_functions
38+
|> List.map_try!(
39+
|{ module_name, exposed_functions }|
40+
List.map_try!(
41+
exposed_functions,
42+
|function_name|
43+
if is_function_unused!(module_name, function_name)? then
44+
Ok(NotFound("${module_name}.${function_name}"))
45+
else
46+
Ok(Found("${module_name}.${function_name}")),
47+
),
48+
)?
49+
50+
not_found_functions =
51+
tagged_functions
52+
|> List.join
53+
|> List.map(
54+
|tagged_function|
55+
when tagged_function is
56+
Found _ -> ""
57+
NotFound qualified_function_name -> qualified_function_name,
58+
)
59+
|> List.keep_if(|s| !Str.is_empty(s))
60+
61+
if List.is_empty(not_found_functions) then
62+
Ok({})
63+
else
64+
Stdout.line!("Functions not used in basic-cli/examples:")?
65+
List.for_each_try!(
66+
not_found_functions,
67+
|function_name|
68+
Stdout.line!(function_name),
69+
)?
70+
Err(Exit(1, "I found untested functions, see above."))
71+
72+
is_function_unused! : Str, Str => Result Bool _
73+
is_function_unused! = |module_name, function_name|
74+
function_pattern = "${module_name}.${function_name}"
75+
search_dir = "basic-cli/examples"
76+
77+
# Use ripgrep to search for the function pattern
78+
cmd =
79+
Cmd.new("rg")
80+
|> Cmd.arg("-q") # Quiet mode - we only care about exit code
81+
|> Cmd.arg(function_pattern)
82+
|> Cmd.arg(search_dir)
83+
84+
status_res = Cmd.status!(cmd)
85+
86+
# ripgrep returns status 0 if matches were found, 1 if no matches
87+
when status_res is
88+
Ok(0) -> Ok(Bool.false) # Function is used (not unused)
89+
_ -> Ok(Bool.true)
90+
91+
process_module! : Str => Result { module_name : Str, exposed_functions : List Str } _
92+
process_module! = |module_name|
93+
module_path = "basic-cli/platform/${module_name}.roc"
94+
95+
module_source_code =
96+
File.read_utf8!(module_path)?
97+
98+
module_items =
99+
extract_module_list(module_source_code)?
100+
101+
module_functions =
102+
module_items
103+
|> List.keep_if(starts_with_lowercase)
104+
105+
Ok({ module_name, exposed_functions: module_functions })
106+
107+
expect
108+
input =
109+
"""
110+
exposes [
111+
Path,
112+
File,
113+
Http
114+
]
115+
"""
116+
117+
output =
118+
extract_exposes_list(input)
119+
120+
output == Ok(["Path", "File", "Http"])
121+
122+
# extra comma
123+
expect
124+
input =
125+
"""
126+
exposes [
127+
Path,
128+
File,
129+
Http,
130+
]
131+
"""
132+
133+
output = extract_exposes_list(input)
134+
135+
output == Ok(["Path", "File", "Http"])
136+
137+
# single line
138+
expect
139+
input = "exposes [Path, File, Http]"
140+
141+
output = extract_exposes_list(input)
142+
143+
output == Ok(["Path", "File", "Http"])
144+
145+
# empty list
146+
expect
147+
input = "exposes []"
148+
149+
output = extract_exposes_list(input)
150+
151+
output == Ok([])
152+
153+
# multiple spaces
154+
expect
155+
input = "exposes [Path]"
156+
157+
output = extract_exposes_list(input)
158+
159+
output == Ok(["Path"])
160+
161+
extract_exposes_list : Str -> Result (List Str) _
162+
extract_exposes_list = |source_code|
163+
164+
when Str.split_first(source_code, "exposes") is
165+
Ok { after } ->
166+
trimmed_after = Str.trim(after)
167+
168+
if Str.starts_with(trimmed_after, "[") then
169+
list_content = Str.replace_first(trimmed_after, "[", "")
170+
171+
when Str.split_first(list_content, "]") is
172+
Ok { before } ->
173+
modules =
174+
before
175+
|> Str.split_on(",")
176+
|> List.map(Str.trim)
177+
|> List.keep_if(|s| !Str.is_empty(s))
178+
179+
Ok(modules)
180+
181+
Err _ ->
182+
err_s("Could not find closing bracket for exposes list in source code:\n\t${source_code}")
183+
else
184+
err_s("Could not find opening bracket after 'exposes' in source code:\n\t${source_code}")
185+
186+
Err _ ->
187+
err_s("Could not find exposes section in source_code:\n\t${source_code}")
188+
189+
expect
190+
input =
191+
"""
192+
module [
193+
Path,
194+
display,
195+
from_str,
196+
IOErr
197+
]
198+
"""
199+
200+
output = extract_module_list(input)
201+
202+
output == Ok(["Path", "display", "from_str", "IOErr"])
203+
204+
# extra comma
205+
expect
206+
input =
207+
"""
208+
module [
209+
Path,
210+
display,
211+
from_str,
212+
IOErr,
213+
]
214+
"""
215+
216+
output = extract_module_list(input)
217+
218+
output == Ok(["Path", "display", "from_str", "IOErr"])
219+
220+
expect
221+
input =
222+
"module [Path, display, from_str, IOErr]"
223+
224+
output = extract_module_list(input)
225+
226+
output == Ok(["Path", "display", "from_str", "IOErr"])
227+
228+
expect
229+
input =
230+
"module []"
231+
232+
output = extract_module_list(input)
233+
234+
output == Ok([])
235+
236+
# with extra space
237+
expect
238+
input =
239+
"module [Path]"
240+
241+
output = extract_module_list(input)
242+
243+
output == Ok(["Path"])
244+
245+
extract_module_list : Str -> Result (List Str) _
246+
extract_module_list = |source_code|
247+
248+
when Str.split_first(source_code, "module") is
249+
Ok { after } ->
250+
trimmed_after = Str.trim(after)
251+
252+
if Str.starts_with(trimmed_after, "[") then
253+
list_content = Str.replace_first(trimmed_after, "[", "")
254+
255+
when Str.split_first(list_content, "]") is
256+
Ok { before } ->
257+
items =
258+
before
259+
|> Str.split_on(",")
260+
|> List.map(Str.trim)
261+
|> List.keep_if(|s| !Str.is_empty(s))
262+
263+
Ok(items)
264+
265+
Err _ ->
266+
err_s("Could not find closing bracket for module list in source code:\n\t${source_code}")
267+
else
268+
err_s("Could not find opening bracket after 'module' in source code:\n\t${source_code}")
269+
270+
Err _ ->
271+
err_s("Could not find module section in source_code:\n\t${source_code}")
272+
273+
expect starts_with_lowercase("hello") == Bool.true
274+
expect starts_with_lowercase("Hello") == Bool.false
275+
expect starts_with_lowercase("!hello") == Bool.false
276+
expect starts_with_lowercase("") == Bool.false
277+
278+
starts_with_lowercase : Str -> Bool
279+
starts_with_lowercase = |str|
280+
if Str.is_empty(str) then
281+
Bool.false
282+
else
283+
first_char_byte =
284+
str
285+
|> Str.to_utf8
286+
|> List.first
287+
|> impossible_err("We verified that the string is not empty")
288+
289+
# ASCII lowercase letters range from 97 ('a') to 122 ('z')
290+
first_char_byte >= 97 and first_char_byte <= 122
291+
292+
impossible_err = |result, err_msg|
293+
when result is
294+
Ok something ->
295+
something
296+
297+
Err err ->
298+
crash "This should have been impossible: ${err_msg}.\n\tError was: ${Inspect.to_str(err)}"

flake.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)