• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ArkScript-lang / Ark / 17100280902

20 Aug 2025 01:45PM UTC coverage: 87.348% (-0.08%) from 87.426%
17100280902

push

github

SuperFola
feat(tests): adding parser tests to improve coverage

4 of 4 new or added lines in 1 file covered. (100.0%)

171 existing lines in 10 files now uncovered.

7553 of 8647 relevant lines covered (87.35%)

129750.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.35
/src/arkscript/Formatter.cpp
1
#include <Ark/Constants.hpp>
2
#include <CLI/Formatter.hpp>
3

4
#include <fmt/core.h>
5
#include <fmt/color.h>
6

7
#include <Ark/Utils/Files.hpp>
8
#include <Ark/Error/Exceptions.hpp>
9
#include <Ark/Error/Diagnostics.hpp>
10
#include <Ark/Compiler/Common.hpp>
11

12
using namespace Ark;
13
using namespace Ark::internal;
14

15
Formatter::Formatter(const bool dry_run) :
46✔
16
    m_dry_run(dry_run), m_parser(/* debug= */ 0, /* interpret= */ false), m_updated(false)
23✔
17
{}
46✔
18

19
Formatter::Formatter(std::string filename, const bool dry_run) :
46✔
20
    m_filename(std::move(filename)), m_dry_run(dry_run), m_parser(/* debug= */ 0, /* interpret= */ false), m_updated(false)
23✔
21
{}
46✔
22

23
void Formatter::run()
23✔
24
{
23✔
25
    try
26
    {
27
        const std::string code = Utils::readFile(m_filename);
23✔
28
        m_parser.process(m_filename, code);
23✔
29
        processAst(m_parser.ast());
23✔
30
        warnIfCommentsWereRemoved(code, ARK_NO_NAME_FILE);
23✔
31

32
        m_updated = code != m_output;
23✔
33
    }
23✔
34
    catch (const CodeError& e)
35
    {
UNCOV
36
        Diagnostics::generate(e);
×
37
    }
23✔
38
}
23✔
39

40
void Formatter::runWithString(const std::string& code)
23✔
41
{
23✔
42
    try
43
    {
44
        m_parser.process(ARK_NO_NAME_FILE, code);
23✔
45
        processAst(m_parser.ast());
23✔
46
        warnIfCommentsWereRemoved(code, ARK_NO_NAME_FILE);
23✔
47

48
        m_updated = code != m_output;
23✔
49
    }
23✔
50
    catch (const CodeError& e)
51
    {
UNCOV
52
        Diagnostics::generate(e);
×
53
    }
23✔
54
}
23✔
55

56
const std::string& Formatter::output() const
46✔
57
{
46✔
58
    return m_output;
46✔
59
}
60

61
bool Formatter::codeModified() const
×
62
{
×
UNCOV
63
    return m_updated;
×
64
}
65

66
void Formatter::processAst(const Node& ast)
46✔
67
{
46✔
68
    // remove useless surrounding begin (generated by the parser)
69
    if (isBeginBlock(ast))
46✔
70
    {
71
        for (std::size_t i = 1, end = ast.constList().size(); i < end; ++i)
184✔
72
        {
73
            const Node node = ast.constList()[i];
138✔
74
            if (shouldAddNewLineBetweenNodes(ast, i) && !m_output.empty())
138✔
75
                m_output += "\n";
18✔
76
            m_output += format(node, 0, false) + "\n";
138✔
77
        }
138✔
78
    }
46✔
79
    else
UNCOV
80
        m_output = format(ast, 0, false);
×
81

82
    if (!m_dry_run)
46✔
83
    {
84
        std::ofstream stream(m_filename);
×
85
        stream << m_output;
×
UNCOV
86
    }
×
87
}
46✔
88

89
void Formatter::warnIfCommentsWereRemoved(const std::string& original_code, const std::string& filename)
46✔
90
{
46✔
91
    if (std::ranges::count(original_code, '#') != std::ranges::count(m_output, '#'))
46✔
92
    {
93
        fmt::println(
×
94
            "{}: one or more comments from the original source code seem to have been removed by mistake while formatting {}",
×
95
            fmt::styled("Warning", fmt::fg(fmt::color::dark_orange)),
×
96
            filename != ARK_NO_NAME_FILE ? filename : "file");
×
97
        fmt::println("Please fill an issue on GitHub: https://github.com/ArkScript-lang/Ark");
×
UNCOV
98
    }
×
99
}
46✔
100

101
bool Formatter::isListStartingWithKeyword(const Node& node, const Keyword keyword)
298✔
102
{
298✔
103
    return node.isListLike() && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Keyword && node.constList()[0].keyword() == keyword;
298✔
104
}
105

106
bool Formatter::isBeginBlock(const Node& node)
212✔
107
{
212✔
108
    return isListStartingWithKeyword(node, Keyword::Begin);
212✔
109
}
110

111
bool Formatter::isFuncDef(const Node& node)
64✔
112
{
64✔
113
    return isListStartingWithKeyword(node, Keyword::Fun);
64✔
114
}
115

116
bool Formatter::isFuncCall(const Node& node)
142✔
117
{
142✔
118
    return node.isListLike() && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Symbol;
142✔
119
}
120

121
std::size_t Formatter::lineOfLastNodeIn(const Node& node)
322✔
122
{
322✔
123
    if (node.isListLike() && !node.constList().empty())
322✔
124
    {
125
        const std::size_t child_line = lineOfLastNodeIn(node.constList().back());
186✔
126
        if (child_line < node.position().start.line)
186✔
127
            return node.position().start.line;
2✔
128
        return child_line;
184✔
129
    }
186✔
130
    return node.position().start.line;
136✔
131
}
322✔
132

133
bool Formatter::shouldSplitOnNewline(const Node& node)
176✔
134
{
176✔
135
    const std::string formatted = format(node, 0, false);
176✔
136
    const std::string::size_type sz = formatted.find_first_of('\n');
176✔
137

138
    const bool is_long_line = !((sz < FormatterConfig::LongLineLength || (sz == std::string::npos && formatted.size() < FormatterConfig::LongLineLength)));
176✔
139
    if (node.comment().empty() && (isBeginBlock(node) || isFuncCall(node)))
176✔
140
        return false;
64✔
141
    if (is_long_line || (node.isListLike() && node.constList().size() > 1) || !node.comment().empty())
112✔
142
        return true;
24✔
143
    return false;
88✔
144
}
176✔
145

146
bool Formatter::shouldAddNewLineBetweenNodes(const Node& node, const std::size_t at)
232✔
147
{
232✔
148
    if (at <= 1)
232✔
149
        return false;
96✔
150

151
    const auto& list = node.constList();
136✔
152
    std::size_t previous_line = lineOfLastNodeIn(list[at - 1]);
136✔
153

154
    const auto& child = list[at];
136✔
155

156
    // If we have a node before the current one,
157
    // and the line count between the two nodes is more than 1,
158
    // maybe we should add a new line to preserve user spacing.
159
    // However, if the current node has a comment, do not add a new line, this is causing the spacing.
160
    if (child.position().start.line - previous_line > 1 && child.comment().empty())
136✔
161
        return true;
16✔
162
    // If we do have a comment but the spacing is more than 2,
163
    // then add a newline to preserve user spacing.
164
    if (child.position().start.line - previous_line > 2 && !child.comment().empty())
120✔
165
        return true;
2✔
166
    return false;
118✔
167
}
232✔
168

169
std::string Formatter::format(const Node& node, std::size_t indent, bool after_newline)
1,522✔
170
{
1,522✔
171
    std::string result;
1,522✔
172
    if (!node.comment().empty())
1,522✔
173
    {
174
        result += formatComment(node.comment(), indent);
66✔
175
        after_newline = true;
66✔
176
    }
66✔
177
    if (after_newline)
1,522✔
178
        result += prefix(indent);
264✔
179

180
    switch (node.nodeType())
1,522✔
181
    {
708✔
182
        case NodeType::Symbol:
183
            result += node.string();
708✔
184
            break;
786✔
185
        case NodeType::Capture:
186
            result += "&" + node.string();
78✔
187
            break;
78✔
188
        case NodeType::Keyword:
UNCOV
189
            result += std::string(keywords[static_cast<std::size_t>(node.keyword())]);
×
190
            break;
42✔
191
        case NodeType::String:
192
            result += fmt::format("\"{}\"", node.string());
42✔
193
            break;
206✔
194
        case NodeType::Number:
195
            result += fmt::format("{}", node.number());
164✔
196
            break;
640✔
197
        case NodeType::List:
198
            result += formatBlock(node, indent, after_newline);
476✔
199
            break;
490✔
200
        case NodeType::Spread:
201
            result += fmt::format("...{}", node.string());
14✔
202
            break;
32✔
203
        case NodeType::Field:
204
        {
205
            std::string field = format(node.constList()[0], indent, false);
18✔
206
            for (std::size_t i = 1, end = node.constList().size(); i < end; ++i)
62✔
207
                field += "." + format(node.constList()[i], indent, false);
44✔
208
            result += field;
18✔
209
            break;
210
        }
40✔
211
        case NodeType::Macro:
212
            result += formatMacro(node, indent);
22✔
213
            break;
22✔
214
        // not handling Namespace nor Unused node types as those can not be generated by the parser
215
        case NodeType::Namespace:
216
            [[fallthrough]];
217
        case NodeType::Unused:
UNCOV
218
            break;
×
219
    }
1,522✔
220

221
    if (!node.commentAfter().empty())
1,522✔
222
        result += " " + formatComment(node.commentAfter(), /* indent= */ 0);
42✔
223

224
    return result;
1,522✔
225
}
1,522✔
226

227
std::string Formatter::formatComment(const std::string& comment, const std::size_t indent) const
120✔
228
{
120✔
229
    std::string result = prefix(indent);
120✔
230
    for (std::size_t i = 0, end = comment.size(); i < end; ++i)
1,732✔
231
    {
232
        result += comment[i];
1,612✔
233
        if (comment[i] == '\n' && i != end - 1)
1,612✔
234
            result += prefix(indent);
6✔
235
    }
1,612✔
236

237
    return result;
120✔
238
}
120✔
239

240
std::string Formatter::formatBlock(const Node& node, const std::size_t indent, const bool after_newline)
476✔
241
{
476✔
242
    if (node.constList().empty())
476✔
243
        return "()";
26✔
244

245
    const Node first = node.constList().front();
450✔
246
    if (first.nodeType() == NodeType::Keyword)
450✔
247
    {
248
        switch (first.keyword())
230✔
249
        {
32✔
250
            case Keyword::Fun:
251
                return formatFunction(node, indent);
96✔
252
            case Keyword::Let:
253
                [[fallthrough]];
254
            case Keyword::Mut:
255
                [[fallthrough]];
256
            case Keyword::Set:
257
                return formatVariable(node, indent);
106✔
258
            case Keyword::If:
259
                return formatCondition(node, indent);
52✔
260
            case Keyword::While:
261
                return formatLoop(node, indent);
68✔
262
            case Keyword::Begin:
263
                return formatBegin(node, indent, after_newline);
78✔
264
            case Keyword::Import:
265
                return formatImport(node, indent);
24✔
266
            case Keyword::Del:
267
                return formatDel(node, indent);
4✔
UNCOV
268
        }
×
269
        // HACK: should never reach, but the compiler insists that the function doesn't return in every code path
UNCOV
270
        return "";
×
271
    }
272
    return formatCall(node, indent);
220✔
273
}
476✔
274

275
std::string Formatter::formatFunction(const Node& node, const std::size_t indent)
32✔
276
{
32✔
277
    const Node args_node = node.constList()[1];
32✔
278
    const Node body_node = node.constList()[2];
32✔
279

280
    std::string formatted_args;
32✔
281

282
    if (!args_node.comment().empty())
32✔
283
    {
284
        formatted_args += "\n";
2✔
285
        formatted_args += formatComment(args_node.comment(), indent + 1);
2✔
286
        formatted_args += prefix(indent + 1);
2✔
287
    }
2✔
288
    else
289
        formatted_args += " ";
30✔
290

291
    if (args_node.isListLike())
32✔
292
    {
293
        bool comment_in_args = false;
30✔
294
        std::string args;
30✔
295
        const bool split = shouldSplitOnNewline(args_node);
30✔
296

297
        for (std::size_t i = 0, end = args_node.constList().size(); i < end; ++i)
90✔
298
        {
299
            const Node arg_i = args_node.constList()[i];
60✔
300
            if (!arg_i.comment().empty())
60✔
301
                comment_in_args = true;
4✔
302

303
            args += format(arg_i, indent + ((comment_in_args || split) ? 1 : 0), i > 0 && (comment_in_args || split));
102✔
304
            if (i != end - 1)
60✔
305
                args += (comment_in_args || split) ? '\n' : ' ';
42✔
306
        }
60✔
307

308
        formatted_args += fmt::format("({}{})", (comment_in_args ? "\n" : ""), args);
30✔
309
    }
30✔
310
    else
311
        formatted_args += format(args_node, indent, false);
2✔
312

313
    if (!shouldSplitOnNewline(body_node) && args_node.comment().empty())
32✔
314
        return fmt::format("(fun{} {})", formatted_args, format(body_node, indent + 1, false));
20✔
315
    return fmt::format("(fun{}\n{})", formatted_args, format(body_node, indent + 1, true));
12✔
316
}
32✔
317

318
std::string Formatter::formatVariable(const Node& node, const std::size_t indent)
64✔
319
{
64✔
320
    std::string keyword = std::string(keywords[static_cast<std::size_t>(node.constList()[0].keyword())]);
64✔
321

322
    const Node body_node = node.constList()[2];
64✔
323
    const std::string formatted_bind = format(node.constList()[1], indent, false);
64✔
324

325
    // we don't want to add another indentation level here, because it would result in a (let a (fun ()\n{indent+=4}...))
326
    if (isFuncDef(body_node))
64✔
327
        return fmt::format("({} {} {})", keyword, formatted_bind, format(body_node, indent, false));
6✔
328
    if (!shouldSplitOnNewline(body_node))
58✔
329
        return fmt::format("({} {} {})", keyword, formatted_bind, format(body_node, indent + 1, false));
56✔
330
    return fmt::format("({} {}\n{})", keyword, formatted_bind, format(body_node, indent + 1, true));
2✔
331
}
64✔
332

333
std::string Formatter::formatCondition(const Node& node, const std::size_t indent, const bool is_macro)
48✔
334
{
48✔
335
    const Node cond_node = node.constList()[1];
48✔
336
    const Node then_node = node.constList()[2];
48✔
337

338
    bool cond_on_newline = false;
48✔
339
    std::string formatted_cond = format(cond_node, indent + 1, false);
48✔
340
    if (formatted_cond.find('\n') != std::string::npos)
48✔
341
        cond_on_newline = true;
2✔
342

343
    std::string if_cond_formatted = fmt::format(
96✔
344
        "({}if{}{}",
48✔
345
        is_macro ? "$" : "",
48✔
346
        cond_on_newline ? "\n" : " ",
48✔
347
        formatted_cond);
348

349
    const bool split_then_newline = shouldSplitOnNewline(then_node);
48✔
350

351
    // (if cond then)
352
    if (node.constList().size() == 3)
48✔
353
    {
354
        if (cond_on_newline || split_then_newline)
22✔
355
            return fmt::format("{}\n{})", if_cond_formatted, format(then_node, indent + 1, true));
2✔
356
        return fmt::format("{} {})", if_cond_formatted, format(then_node, indent + 1, false));
20✔
357
    }
358
    // (if cond then else)
359
    return fmt::format(
26✔
360
        "{}\n{}\n{}{})",
26✔
361
        if_cond_formatted,
362
        format(then_node, indent + 1, true),
26✔
363
        format(node.constList()[3], indent + 1, true),
26✔
364
        node.constList()[3].commentAfter().empty() ? "" : ("\n" + prefix(indent)));
26✔
365
}
48✔
366

367
std::string Formatter::formatLoop(const Node& node, const std::size_t indent)
10✔
368
{
10✔
369
    const Node cond_node = node.constList()[1];
10✔
370
    const Node body_node = node.constList()[2];
10✔
371

372
    bool cond_on_newline = false;
10✔
373
    std::string formatted_cond = format(cond_node, indent + 1, false);
10✔
374
    if (formatted_cond.find('\n') != std::string::npos)
10✔
375
        cond_on_newline = true;
2✔
376

377
    if (cond_on_newline || shouldSplitOnNewline(body_node))
10✔
378
        return fmt::format(
8✔
379
            "(while{}{}\n{})",
4✔
380
            cond_on_newline ? "\n" : " ",
4✔
381
            formatted_cond,
382
            format(body_node, indent + 1, true));
4✔
383
    return fmt::format(
6✔
384
        "(while {} {})",
6✔
385
        formatted_cond,
386
        format(body_node, indent + 1, false));
6✔
387
}
10✔
388

389
std::string Formatter::formatBegin(const Node& node, const std::size_t indent, const bool after_newline)
58✔
390
{
58✔
391
    // only the keyword begin is present
392
    if (node.constList().size() == 1)
58✔
393
        return "{}";
8✔
394

395
    // after a new line, we need to increment our indentation level
396
    // if the block is a top level one, we also need to increment indentation level
397
    const std::size_t inner_indentation = indent + (after_newline ? 1 : 0) + (indent == 0 ? 1 : 0);
50✔
398

399
    std::string result = "{\n";
50✔
400
    // skip begin keyword
401
    for (std::size_t i = 1, end = node.constList().size(); i < end; ++i)
144✔
402
    {
403
        const Node child = node.constList()[i];
94✔
404
        // we want to preserve the node grouping by the user, but remove useless duplicate new line
405
        // but that shouldn't apply to the first node of the block
406
        if (shouldAddNewLineBetweenNodes(node, i) && i > 1)
94✔
UNCOV
407
            result += "\n";
×
408

409
        result += format(child, inner_indentation, true);
94✔
410
        if (i != end - 1)
94✔
411
            result += "\n";
44✔
412
    }
94✔
413

414
    // if the last node has a comment, add a new line
415
    if (!node.constList().empty() && !node.constList().back().commentAfter().empty())
50✔
416
        result += "\n" + prefix(indent) + "}";
2✔
417
    else
418
        result += " }";
48✔
419
    return result;
50✔
420
}
58✔
421

422
std::string Formatter::formatImport(const Node& node, const std::size_t indent)
20✔
423
{
20✔
424
    const Node package_node = node.constList()[1];
20✔
425
    std::string package;
20✔
426

427
    if (!package_node.comment().empty())
20✔
428
        package += "\n" + formatComment(package_node.comment(), indent + 1) + prefix(indent + 1);
2✔
429
    else
430
        package += " ";
18✔
431

432
    for (std::size_t i = 0, end = package_node.constList().size(); i < end; ++i)
58✔
433
    {
434
        package += format(package_node.constList()[i], indent + 1, false);
38✔
435
        if (i != end - 1)
38✔
436
            package += ".";
18✔
437
    }
38✔
438

439
    const Node symbols = node.constList()[2];
20✔
440
    if (symbols.nodeType() == NodeType::Symbol && symbols.string() == "*")
20✔
441
        package += ":*";
2✔
442
    else  // symbols is a list
443
    {
444
        if (const auto& sym_list = symbols.constList(); !sym_list.empty())
28✔
445
        {
446
            const bool comment_after_last = !sym_list.back().commentAfter().empty();
10✔
447

448
            for (const auto& sym : sym_list)
24✔
449
            {
450
                if (sym.comment().empty())
14✔
451
                {
452
                    if (comment_after_last)
8✔
453
                        package += "\n" + prefix(indent + 1) + ":" + sym.string();
2✔
454
                    else
455
                        package += " :" + sym.string();
6✔
456
                }
8✔
457
                else
458
                    package += "\n" + formatComment(sym.comment(), indent + 1) + prefix(indent + 1) + ":" + sym.string();
6✔
459
            }
14✔
460

461
            if (comment_after_last)
10✔
462
            {
463
                package += " " + formatComment(sym_list.back().commentAfter(), /* indent= */ 0);
2✔
464
                package += "\n" + prefix(indent + 1);
2✔
465
            }
2✔
466
        }
10✔
467
    }
468

469
    return fmt::format("(import{})", package);
20✔
470
}
20✔
471

472
std::string Formatter::formatDel(const Node& node, const std::size_t indent)
4✔
473
{
4✔
474
    std::string formatted_sym = format(node.constList()[1], indent + 1, false);
4✔
475
    if (formatted_sym.find('\n') != std::string::npos)
4✔
476
        return fmt::format("(del\n{})", formatted_sym);
2✔
477
    return fmt::format("(del {})", formatted_sym);
2✔
478
}
4✔
479

480
std::string Formatter::formatCall(const Node& node, const std::size_t indent)
220✔
481
{
220✔
482
    bool is_list = false;
220✔
483
    if (!node.constList().empty() && node.constList().front().nodeType() == NodeType::Symbol &&
412✔
484
        node.constList().front().string() == "list")
192✔
485
        is_list = true;
4✔
486

487
    bool is_multiline = false;
220✔
488

489
    std::vector<std::string> formatted_args;
220✔
490
    for (std::size_t i = 1, end = node.constList().size(); i < end; ++i)
580✔
491
    {
492
        formatted_args.push_back(format(node.constList()[i], indent, false));
360✔
493
        // if we have at least one argument taking multiple lines, split them all on their own line
494
        if (formatted_args.back().find('\n') != std::string::npos || !node.constList()[i].commentAfter().empty())
360✔
495
            is_multiline = true;
14✔
496
    }
360✔
497

498
    std::string result = is_list ? "[" : ("(" + format(node.constList()[0], indent, false));
220✔
499
    for (std::size_t i = 0, end = formatted_args.size(); i < end; ++i)
580✔
500
    {
501
        const std::string formatted_node = formatted_args[i];
360✔
502
        if (is_multiline)
360✔
503
            result += "\n" + format(node.constList()[i + 1], indent + 1, true);
24✔
504
        else
505
            result += (is_list && i == 0 ? "" : " ") + formatted_node;
336✔
506
    }
360✔
507
    if (!node.constList().back().commentAfter().empty())
220✔
508
        result += "\n" + prefix(indent);
6✔
509
    result += is_list ? "]" : ")";
220✔
510
    return result;
220✔
511
}
220✔
512

513
std::string Formatter::formatMacro(const Node& node, const std::size_t indent)
22✔
514
{
22✔
515
    if (isListStartingWithKeyword(node, Keyword::If))
22✔
516
        return formatCondition(node, indent, /* is_macro= */ true);
6✔
517

518
    std::string result = "(macro ";
16✔
519
    bool after_newline = false;
16✔
520

521
    for (std::size_t i = 0, end = node.constList().size(); i < end; ++i)
62✔
522
    {
523
        result += format(node.constList()[i], indent + 1, after_newline);
46✔
524
        after_newline = false;
46✔
525

526
        if (!node.constList()[i].commentAfter().empty())
46✔
527
        {
528
            result += "\n";
6✔
529
            after_newline = true;
6✔
530
        }
6✔
531
        else if (i != end - 1)
40✔
532
            result += " ";
26✔
533
    }
46✔
534

535
    return result + ")";
16✔
536
}
22✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc