Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Roblox
GitHub Repository: Roblox/luau
Path: blob/master/CLI/src/Analyze.cpp
2725 views
1
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
2
#include "Luau/BuiltinDefinitions.h"
3
#include "Luau/Config.h"
4
#include "Luau/Frontend.h"
5
#include "Luau/LuauConfig.h"
6
#include "Luau/ModuleResolver.h"
7
#include "Luau/PrettyPrinter.h"
8
#include "Luau/StringUtils.h"
9
#include "Luau/TimeTrace.h"
10
#include "Luau/TypeAttach.h"
11
#include "Luau/TypeCheckLimits.h"
12
#include "Luau/TypeInfer.h"
13
14
#include "Luau/AnalyzeRequirer.h"
15
#include "Luau/FileUtils.h"
16
#include "Luau/Flags.h"
17
#include "Luau/RequireNavigator.h"
18
19
#include "lua.h"
20
#include "lualib.h"
21
22
#include <condition_variable>
23
#include <functional>
24
#include <mutex>
25
#include <queue>
26
#include <thread>
27
#include <utility>
28
#include <fstream>
29
30
#ifdef CALLGRIND
31
#include <valgrind/callgrind.h>
32
#endif
33
34
LUAU_FASTFLAG(DebugLuauTimeTracing)
35
LUAU_FASTFLAG(DebugLuauLogSolverToJsonFile)
36
37
enum class ReportFormat
38
{
39
Default,
40
Luacheck,
41
Gnu,
42
};
43
44
static void report(ReportFormat format, const char* name, const Luau::Location& loc, const char* type, const char* message)
45
{
46
switch (format)
47
{
48
case ReportFormat::Default:
49
fprintf(stderr, "%s(%d,%d): %s: %s\n", name, loc.begin.line + 1, loc.begin.column + 1, type, message);
50
break;
51
52
case ReportFormat::Luacheck:
53
{
54
// Note: luacheck's end column is inclusive but our end column is exclusive
55
// In addition, luacheck doesn't support multi-line messages, so if the error is multiline we'll fake end column as 100 and hope for the best
56
int columnEnd = (loc.begin.line == loc.end.line) ? loc.end.column : 100;
57
58
// Use stdout to match luacheck behavior
59
fprintf(stdout, "%s:%d:%d-%d: (W0) %s: %s\n", name, loc.begin.line + 1, loc.begin.column + 1, columnEnd, type, message);
60
break;
61
}
62
63
case ReportFormat::Gnu:
64
// Note: GNU end column is inclusive but our end column is exclusive
65
fprintf(stderr, "%s:%d.%d-%d.%d: %s: %s\n", name, loc.begin.line + 1, loc.begin.column + 1, loc.end.line + 1, loc.end.column, type, message);
66
break;
67
}
68
}
69
70
static void reportError(const Luau::Frontend& frontend, ReportFormat format, const Luau::TypeError& error)
71
{
72
std::string humanReadableName = frontend.fileResolver->getHumanReadableModuleName(error.moduleName);
73
74
if (const Luau::SyntaxError* syntaxError = Luau::get_if<Luau::SyntaxError>(&error.data))
75
report(format, humanReadableName.c_str(), error.location, "SyntaxError", syntaxError->message.c_str());
76
else
77
report(
78
format,
79
humanReadableName.c_str(),
80
error.location,
81
"TypeError",
82
Luau::toString(error, Luau::TypeErrorToStringOptions{frontend.fileResolver}).c_str()
83
);
84
}
85
86
static void reportWarning(ReportFormat format, const char* name, const Luau::LintWarning& warning)
87
{
88
report(format, name, warning.location, Luau::LintWarning::getName(warning.code), warning.text.c_str());
89
}
90
91
static bool reportModuleResult(Luau::Frontend& frontend, const Luau::ModuleName& name, ReportFormat format, bool annotate)
92
{
93
std::optional<Luau::CheckResult> cr = frontend.getCheckResult(name, false);
94
95
if (!cr)
96
{
97
fprintf(stderr, "Failed to find result for %s\n", name.c_str());
98
return false;
99
}
100
101
if (!frontend.getSourceModule(name))
102
{
103
fprintf(stderr, "Error opening %s\n", name.c_str());
104
return false;
105
}
106
107
for (auto& error : cr->errors)
108
reportError(frontend, format, error);
109
110
std::string humanReadableName = frontend.fileResolver->getHumanReadableModuleName(name);
111
for (auto& error : cr->lintResult.errors)
112
reportWarning(format, humanReadableName.c_str(), error);
113
for (auto& warning : cr->lintResult.warnings)
114
reportWarning(format, humanReadableName.c_str(), warning);
115
116
if (annotate)
117
{
118
Luau::SourceModule* sm = frontend.getSourceModule(name);
119
Luau::ModulePtr m = frontend.moduleResolver.getModule(name);
120
121
Luau::attachTypeData(*sm, *m);
122
123
std::string annotated = Luau::prettyPrintWithTypes(*sm->root);
124
125
printf("%s", annotated.c_str());
126
}
127
128
return cr->errors.empty() && cr->lintResult.errors.empty();
129
}
130
131
static void displayHelp(const char* argv0)
132
{
133
printf("Usage: %s [--mode] [options] [file list]\n", argv0);
134
printf("\n");
135
printf("Available modes:\n");
136
printf(" omitted: typecheck and lint input files\n");
137
printf(" --annotate: typecheck input files and output source with type annotations\n");
138
printf("\n");
139
printf("Available options:\n");
140
printf(" --formatter=plain: report analysis errors in Luacheck-compatible format\n");
141
printf(" --formatter=gnu: report analysis errors in GNU-compatible format\n");
142
printf(" --mode=strict: default to strict mode when typechecking\n");
143
printf(" --timetrace: record compiler time tracing information into trace.json\n");
144
}
145
146
static int assertionHandler(const char* expr, const char* file, int line, const char* function)
147
{
148
printf("%s(%d): ASSERTION FAILED: %s\n", file, line, expr);
149
fflush(stdout);
150
return 1;
151
}
152
153
struct LuauConfigInterruptInfo
154
{
155
Luau::TypeCheckLimits limits;
156
std::string module;
157
};
158
159
struct CliFileResolver : Luau::FileResolver
160
{
161
std::optional<Luau::SourceCode> readSource(const Luau::ModuleName& name) override
162
{
163
Luau::SourceCode::Type sourceType;
164
std::optional<std::string> source = std::nullopt;
165
166
// If the module name is "-", then read source from stdin
167
if (name == "-")
168
{
169
source = readStdin();
170
sourceType = Luau::SourceCode::Script;
171
}
172
else
173
{
174
source = readFile(name);
175
sourceType = Luau::SourceCode::Module;
176
}
177
178
if (!source)
179
return std::nullopt;
180
181
return Luau::SourceCode{*source, sourceType};
182
}
183
184
std::optional<Luau::ModuleInfo> resolveModule(const Luau::ModuleInfo* context, Luau::AstExpr* node, const Luau::TypeCheckLimits& limits) override
185
{
186
if (Luau::AstExprConstantString* expr = node->as<Luau::AstExprConstantString>())
187
{
188
std::string path{expr->value.data, expr->value.size};
189
190
FileNavigationContext navigationContext{context->name};
191
192
LuauConfigInterruptInfo info = {limits, path};
193
navigationContext.luauConfigInit = [&info](lua_State* L)
194
{
195
lua_setthreaddata(L, &info);
196
};
197
navigationContext.luauConfigInterrupt = [](lua_State* L, int gc)
198
{
199
LuauConfigInterruptInfo* info = static_cast<LuauConfigInterruptInfo*>(lua_getthreaddata(L));
200
if (info->limits.finishTime && Luau::TimeTrace::getClock() > *info->limits.finishTime)
201
throw Luau::TimeLimitError{info->module};
202
if (info->limits.cancellationToken && info->limits.cancellationToken->requested())
203
throw Luau::UserCancelError{info->module};
204
};
205
206
Luau::Require::ErrorHandler nullErrorHandler{};
207
208
Luau::Require::Navigator navigator(navigationContext, nullErrorHandler);
209
if (navigator.navigate(std::move(path)) != Luau::Require::Navigator::Status::Success)
210
return std::nullopt;
211
212
if (!navigationContext.isModulePresent())
213
return std::nullopt;
214
215
if (std::optional<std::string> identifier = navigationContext.getIdentifier())
216
return {{*identifier}};
217
}
218
219
return std::nullopt;
220
}
221
222
std::string getHumanReadableModuleName(const Luau::ModuleName& name) const override
223
{
224
if (name == "-")
225
return "stdin";
226
return name;
227
}
228
};
229
230
struct CliConfigResolver : Luau::ConfigResolver
231
{
232
Luau::Config defaultConfig;
233
234
mutable std::unordered_map<std::string, Luau::Config> configCache;
235
mutable std::vector<std::pair<std::string, std::string>> configErrors;
236
237
CliConfigResolver(Luau::Mode mode)
238
{
239
defaultConfig.mode = mode;
240
}
241
242
const Luau::Config& getConfig(const Luau::ModuleName& name, const Luau::TypeCheckLimits& limits) const override
243
{
244
std::optional<std::string> path = getParentPath(name);
245
if (!path)
246
return defaultConfig;
247
248
return readConfigRec(*path, limits);
249
}
250
251
const Luau::Config& readConfigRec(const std::string& path, const Luau::TypeCheckLimits& limits) const
252
{
253
auto it = configCache.find(path);
254
if (it != configCache.end())
255
return it->second;
256
257
std::optional<std::string> parent = getParentPath(path);
258
Luau::Config result = parent ? readConfigRec(*parent, limits) : defaultConfig;
259
260
std::optional<std::string> configPath = joinPaths(path, Luau::kConfigName);
261
if (!isFile(*configPath))
262
configPath = std::nullopt;
263
264
std::optional<std::string> luauConfigPath = joinPaths(path, Luau::kLuauConfigName);
265
if (!isFile(*luauConfigPath))
266
luauConfigPath = std::nullopt;
267
268
if (configPath && luauConfigPath)
269
{
270
std::string ambiguousError = Luau::format("Both %s and %s files exist", Luau::kConfigName, Luau::kLuauConfigName);
271
configErrors.emplace_back(*configPath, std::move(ambiguousError));
272
}
273
else if (configPath)
274
{
275
if (std::optional<std::string> contents = readFile(*configPath))
276
{
277
Luau::ConfigOptions::AliasOptions aliasOpts;
278
aliasOpts.configLocation = *configPath;
279
aliasOpts.overwriteAliases = true;
280
281
Luau::ConfigOptions opts;
282
opts.aliasOptions = std::move(aliasOpts);
283
284
std::optional<std::string> error = Luau::parseConfig(*contents, result, opts);
285
if (error)
286
configErrors.emplace_back(*configPath, *error);
287
}
288
}
289
else if (luauConfigPath)
290
{
291
if (std::optional<std::string> contents = readFile(*luauConfigPath))
292
{
293
Luau::ConfigOptions::AliasOptions aliasOpts;
294
aliasOpts.configLocation = *configPath;
295
aliasOpts.overwriteAliases = true;
296
297
Luau::InterruptCallbacks callbacks;
298
LuauConfigInterruptInfo info{limits, *luauConfigPath};
299
callbacks.initCallback = [&info](lua_State* L)
300
{
301
lua_setthreaddata(L, &info);
302
};
303
callbacks.interruptCallback = [](lua_State* L, int gc)
304
{
305
LuauConfigInterruptInfo* info = static_cast<LuauConfigInterruptInfo*>(lua_getthreaddata(L));
306
if (info->limits.finishTime && Luau::TimeTrace::getClock() > *info->limits.finishTime)
307
throw Luau::TimeLimitError{info->module};
308
if (info->limits.cancellationToken && info->limits.cancellationToken->requested())
309
throw Luau::UserCancelError{info->module};
310
};
311
312
std::optional<std::string> error = Luau::extractLuauConfig(*contents, result, aliasOpts, std::move(callbacks));
313
if (error)
314
configErrors.emplace_back(*luauConfigPath, *error);
315
}
316
}
317
318
return configCache[path] = result;
319
}
320
};
321
322
struct TaskScheduler
323
{
324
TaskScheduler(unsigned threadCount)
325
: threadCount(threadCount)
326
{
327
for (unsigned i = 0; i < threadCount; i++)
328
{
329
workers.emplace_back(
330
[this]
331
{
332
workerFunction();
333
}
334
);
335
}
336
}
337
338
~TaskScheduler()
339
{
340
for (unsigned i = 0; i < threadCount; i++)
341
push({});
342
343
for (std::thread& worker : workers)
344
worker.join();
345
}
346
347
std::function<void()> pop()
348
{
349
std::unique_lock guard(mtx);
350
351
cv.wait(
352
guard,
353
[this]
354
{
355
return !tasks.empty();
356
}
357
);
358
359
std::function<void()> task = tasks.front();
360
tasks.pop();
361
return task;
362
}
363
364
void push(std::function<void()> task)
365
{
366
{
367
std::unique_lock guard(mtx);
368
tasks.push(std::move(task));
369
}
370
371
cv.notify_one();
372
}
373
374
static unsigned getThreadCount()
375
{
376
return std::max(std::thread::hardware_concurrency(), 1u);
377
}
378
379
private:
380
void workerFunction()
381
{
382
while (std::function<void()> task = pop())
383
task();
384
}
385
386
unsigned threadCount = 1;
387
std::mutex mtx;
388
std::condition_variable cv;
389
std::vector<std::thread> workers;
390
std::queue<std::function<void()>> tasks;
391
};
392
393
int main(int argc, char** argv)
394
{
395
Luau::assertHandler() = assertionHandler;
396
397
setLuauFlagsDefault();
398
399
if (argc >= 2 && strcmp(argv[1], "--help") == 0)
400
{
401
displayHelp(argv[0]);
402
return 0;
403
}
404
405
ReportFormat format = ReportFormat::Default;
406
Luau::Mode mode = Luau::Mode::Nonstrict;
407
bool annotate = false;
408
int threadCount = 0;
409
std::string basePath = "";
410
411
for (int i = 1; i < argc; ++i)
412
{
413
if (argv[i][0] != '-')
414
continue;
415
416
if (strcmp(argv[i], "--formatter=plain") == 0)
417
format = ReportFormat::Luacheck;
418
else if (strcmp(argv[i], "--formatter=gnu") == 0)
419
format = ReportFormat::Gnu;
420
else if (strcmp(argv[i], "--mode=strict") == 0)
421
mode = Luau::Mode::Strict;
422
else if (strcmp(argv[i], "--annotate") == 0)
423
annotate = true;
424
else if (strcmp(argv[i], "--timetrace") == 0)
425
FFlag::DebugLuauTimeTracing.value = true;
426
else if (strncmp(argv[i], "--fflags=", 9) == 0)
427
setLuauFlags(argv[i] + 9);
428
else if (strncmp(argv[i], "-j", 2) == 0)
429
threadCount = int(strtol(argv[i] + 2, nullptr, 10));
430
else if (strncmp(argv[i], "--logbase=", 10) == 0)
431
basePath = std::string{argv[i] + 10};
432
}
433
434
#if !defined(LUAU_ENABLE_TIME_TRACE)
435
if (FFlag::DebugLuauTimeTracing)
436
{
437
fprintf(stderr, "To run with --timetrace, Luau has to be built with LUAU_ENABLE_TIME_TRACE enabled\n");
438
return 1;
439
}
440
#endif
441
442
Luau::FrontendOptions frontendOptions;
443
frontendOptions.retainFullTypeGraphs = annotate;
444
frontendOptions.runLintChecks = true;
445
446
CliFileResolver fileResolver;
447
CliConfigResolver configResolver(mode);
448
Luau::Frontend frontend(&fileResolver, &configResolver, frontendOptions);
449
450
if (FFlag::DebugLuauLogSolverToJsonFile)
451
{
452
frontend.writeJsonLog = [&basePath](const Luau::ModuleName& moduleName, std::string log)
453
{
454
std::string path = moduleName + ".log.json";
455
size_t pos = moduleName.find_last_of('/');
456
if (pos != std::string::npos)
457
path = moduleName.substr(pos + 1);
458
459
if (!basePath.empty())
460
path = joinPaths(basePath, path);
461
462
std::ofstream os(path);
463
464
os << log << std::endl;
465
printf("Wrote JSON log to %s\n", path.c_str());
466
};
467
}
468
469
Luau::registerBuiltinGlobals(frontend, frontend.globals);
470
Luau::freeze(frontend.globals.globalTypes);
471
472
#ifdef CALLGRIND
473
CALLGRIND_ZERO_STATS;
474
#endif
475
476
std::vector<std::string> files = getSourceFiles(argc, argv);
477
478
for (const std::string& path : files)
479
frontend.queueModuleCheck(path);
480
481
std::vector<Luau::ModuleName> checkedModules;
482
483
// If thread count is not set, try to use HW thread count, but with an upper limit
484
// When we improve scalability of typechecking, upper limit can be adjusted/removed
485
if (threadCount <= 0)
486
threadCount = std::min(TaskScheduler::getThreadCount(), 8u);
487
488
try
489
{
490
TaskScheduler scheduler(threadCount);
491
492
checkedModules = frontend.checkQueuedModules(
493
std::nullopt,
494
[&](std::vector<std::function<void()>> tasks)
495
{
496
for (auto& task : tasks)
497
scheduler.push(std::move(task));
498
}
499
);
500
}
501
catch (const Luau::InternalCompilerError& ice)
502
{
503
Luau::Location location = ice.location ? *ice.location : Luau::Location();
504
505
std::string moduleName = ice.moduleName ? *ice.moduleName : "<unknown module>";
506
std::string humanReadableName = frontend.fileResolver->getHumanReadableModuleName(moduleName);
507
508
Luau::TypeError error(location, moduleName, Luau::InternalError{ice.message});
509
510
report(
511
format,
512
humanReadableName.c_str(),
513
location,
514
"InternalCompilerError",
515
Luau::toString(error, Luau::TypeErrorToStringOptions{frontend.fileResolver}).c_str()
516
);
517
return 1;
518
}
519
520
int failed = 0;
521
522
for (const Luau::ModuleName& name : checkedModules)
523
failed += !reportModuleResult(frontend, name, format, annotate);
524
525
if (!configResolver.configErrors.empty())
526
{
527
failed += int(configResolver.configErrors.size());
528
529
for (const auto& pair : configResolver.configErrors)
530
fprintf(stderr, "%s: %s\n", pair.first.c_str(), pair.second.c_str());
531
}
532
533
if (format == ReportFormat::Luacheck)
534
return 0;
535
else
536
return failed ? 1 : 0;
537
}
538
539