1 /**
2 * Copyright (c) 2023-2024 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16 #include "arktsconfig.h"
17 #include "libpandabase/utils/json_builder.h"
18 #include "libpandabase/utils/json_parser.h"
19 #include "libpandabase/os/filesystem.h"
20 #include "util/language.h"
21 #include "generated/signatures.h"
22
23 #include <fstream>
24 #include <regex>
25 #include <sstream>
26 #include <system_error>
27
28 #ifndef ARKTSCONFIG_USE_FILESYSTEM
29 #include <dirent.h>
30 #include <sys/types.h>
31 #include <unistd.h>
32 #else
33 #if __has_include(<filesystem>)
34 #include <filesystem>
35 namespace fs = std::filesystem;
36 #elif __has_include(<experimental/filesystem>)
37 #include <experimental/filesystem>
38 namespace fs = std::experimental::filesystem;
39 #endif
40 #endif
41
42 namespace ark::es2panda {
43
44 template <class... Ts>
Check(bool cond, const Ts &...msgs)45 static bool Check(bool cond, const Ts &...msgs)
46 {
47 if (!cond) {
48 ((std::cerr << "ArkTsConfig error: ") << ... << msgs);
49 return false;
50 }
51
52 return true;
53 }
54
IsAbsolute(const std::string &path)55 static bool IsAbsolute(const std::string &path)
56 {
57 #ifndef ARKTSCONFIG_USE_FILESYSTEM
58 return !path.empty() && path[0] == '/';
59 #else
60 return fs::path(path).is_absolute();
61 #endif // ARKTSCONFIG_USE_FILESYSTEM
62 }
63
JoinPaths(const std::string &a, const std::string &b)64 std::string JoinPaths(const std::string &a, const std::string &b)
65 {
66 #ifndef ARKTSCONFIG_USE_FILESYSTEM
67 return a + '/' + b;
68 #else
69 return (fs::path(a) / b).string();
70 #endif // ARKTSCONFIG_USE_FILESYSTEM
71 }
72
ParentPath(const std::string &path)73 std::string ParentPath(const std::string &path)
74 {
75 #ifndef ARKTSCONFIG_USE_FILESYSTEM
76 auto pos = path.find('/');
77 return pos == std::string::npos ? path : path.substr(0, pos);
78 #else
79 return fs::path(path).parent_path().string();
80 #endif // ARKTSCONFIG_USE_FILESYSTEM
81 }
82
MakeAbsolute(const std::string &path, const std::string &base)83 static std::string MakeAbsolute(const std::string &path, const std::string &base)
84 {
85 return IsAbsolute(path) ? path : JoinPaths(base, path);
86 }
87
88 #ifdef ARKTSCONFIG_USE_FILESYSTEM
89
Pattern(std::string value, std::string base)90 ArkTsConfig::Pattern::Pattern(std::string value, std::string base) : value_(std::move(value)), base_(std::move(base))
91 {
92 ASSERT(fs::path(base_).is_absolute());
93 }
94
IsPattern() const95 bool ArkTsConfig::Pattern::IsPattern() const
96 {
97 return (value_.find('*') != std::string::npos) || (value_.find('?') != std::string::npos);
98 }
99
GetSearchRoot() const100 std::string ArkTsConfig::Pattern::GetSearchRoot() const
101 {
102 fs::path relative;
103 if (!IsPattern()) {
104 relative = value_;
105 } else {
106 auto foundStar = value_.find_first_of('*');
107 auto foundQuestion = value_.find_first_of('?');
108 relative = value_.substr(0, std::min(foundStar, foundQuestion));
109 relative = relative.parent_path();
110 }
111 return MakeAbsolute(relative.string(), base_);
112 }
113
Match(const std::string &path) const114 bool ArkTsConfig::Pattern::Match(const std::string &path) const
115 {
116 ASSERT(fs::path(path).is_absolute());
117 fs::path value = fs::path(value_);
118 std::string pattern = value.is_absolute() ? value.string() : (base_ / value).string();
119
120 // Replace arktsconfig special symbols with regular expressions
121 if (IsPattern()) {
122 // '**' matches any directory nested to any level
123 pattern = std::regex_replace(pattern, std::regex("\\*\\*/"), ".*");
124 // '*' matches zero or more characters (excluding directory separators)
125 pattern = std::regex_replace(pattern, std::regex("([^\\.])\\*"), "$1[^/]*");
126 // '?' matches any one character (excluding directory separators)
127 pattern = std::regex_replace(pattern, std::regex("\\?"), "[^/]");
128 }
129 if (!value.has_extension()) {
130 // default extensions to match
131 pattern += R"(.*(\.ts|\.d\.ts|\.sts)$)";
132 }
133 std::smatch m;
134 auto res = std::regex_match(path, m, std::regex(pattern));
135 return res;
136 }
137
ResolveConfigLocation(const std::string &relPath, const std::string &base)138 static std::string ResolveConfigLocation(const std::string &relPath, const std::string &base)
139 {
140 auto resolvedPath = MakeAbsolute(relPath, base);
141 auto newBase = base;
142 while (!fs::exists(resolvedPath)) {
143 resolvedPath = MakeAbsolute(relPath, JoinPaths(newBase, "node_modules"));
144 if (newBase == ParentPath(newBase)) {
145 return "";
146 }
147 newBase = ParentPath(newBase);
148 }
149 return resolvedPath;
150 }
151
ParseExtends(const std::string &extends, const std::string &configDir)152 bool ArkTsConfig::ParseExtends(const std::string &extends, const std::string &configDir)
153 {
154 auto basePath = ResolveConfigLocation(extends, configDir);
155 if (!Check(!basePath.empty(), "Can't resolve config path: ", extends)) {
156 return false;
157 }
158
159 auto base = ArkTsConfig(basePath);
160 if (!Check(base.Parse(), "Failed to parse base config: ", extends)) {
161 return false;
162 }
163
164 Inherit(base);
165 return true;
166 }
167 #endif // ARKTSCONFIG_USE_FILESYSTEM
168
ValidDynamicLanguages()169 static std::string ValidDynamicLanguages()
170 {
171 JsonArrayBuilder builder;
172 for (auto &l : Language::All()) {
173 if (l.IsDynamic()) {
174 builder.Add(l.ToString());
175 }
176 }
177 return std::move(builder).Build();
178 }
179
180 template <class PathsMap>
ParsePaths(const JsonObject::JsonObjPointer *options, PathsMap &pathsMap, const std::string &baseUrl)181 static bool ParsePaths(const JsonObject::JsonObjPointer *options, PathsMap &pathsMap, const std::string &baseUrl)
182 {
183 if (options == nullptr) {
184 return true;
185 }
186
187 auto paths = options->get()->GetValue<JsonObject::JsonObjPointer>("paths");
188 if (paths == nullptr) {
189 return true;
190 }
191
192 for (size_t keyIdx = 0; keyIdx < paths->get()->GetSize(); ++keyIdx) {
193 auto &key = paths->get()->GetKeyByIndex(keyIdx);
194 if (pathsMap.count(key) == 0U) {
195 pathsMap.insert({key, {}});
196 }
197
198 auto values = paths->get()->GetValue<JsonObject::ArrayT>(key);
199 if (!Check(values, "Invalid value for 'path' with key '", key, "'")) {
200 return false;
201 }
202
203 if (!Check(!values->empty(), "Substitutions for pattern '", key, "' shouldn't be an empty array")) {
204 return false;
205 }
206
207 for (auto &v : *values) {
208 auto p = *v.Get<JsonObject::StringT>();
209 pathsMap[key].emplace_back(MakeAbsolute(p, baseUrl));
210 }
211 }
212
213 return true;
214 }
215
216 template <class PathsMap>
ParseDynamicPaths(const JsonObject::JsonObjPointer *options, PathsMap &dynamicPathsMap)217 static bool ParseDynamicPaths(const JsonObject::JsonObjPointer *options, PathsMap &dynamicPathsMap)
218 {
219 static const std::string LANGUAGE = "language";
220 static const std::string HAS_DECL = "hasDecl";
221
222 if (options == nullptr) {
223 return true;
224 }
225
226 auto dynamicPaths = options->get()->GetValue<JsonObject::JsonObjPointer>("dynamicPaths");
227 if (dynamicPaths == nullptr) {
228 return true;
229 }
230
231 for (size_t keyIdx = 0; keyIdx < dynamicPaths->get()->GetSize(); ++keyIdx) {
232 auto &key = dynamicPaths->get()->GetKeyByIndex(keyIdx);
233 auto data = dynamicPaths->get()->GetValue<JsonObject::JsonObjPointer>(key);
234 if (!Check(data != nullptr, "Invalid value for for dynamic path with key '", key, "'")) {
235 return false;
236 }
237
238 auto langValue = data->get()->GetValue<JsonObject::StringT>(LANGUAGE);
239 if (!Check(langValue != nullptr, "Invalid '", LANGUAGE, "' value for dynamic path with key '", key, "'")) {
240 return false;
241 }
242
243 auto lang = Language::FromString(*langValue);
244 if (!Check(lang && lang->IsDynamic(), "Invalid '", LANGUAGE, "' value for dynamic path with key '", key,
245 "'. Should be one of ", ValidDynamicLanguages())) {
246 return false;
247 }
248
249 if (!Check(compiler::Signatures::Dynamic::IsSupported(*lang), "Interoperability with language '",
250 lang->ToString(), "' is not supported")) {
251 return false;
252 }
253
254 auto hasDeclValue = data->get()->GetValue<JsonObject::BoolT>(HAS_DECL);
255 if (!Check(hasDeclValue != nullptr, "Invalid '", HAS_DECL, "' value for dynamic path with key '", key, "'")) {
256 return false;
257 }
258
259 auto normalizedKey = ark::os::NormalizePath(key);
260 auto res = dynamicPathsMap.insert({normalizedKey, ArkTsConfig::DynamicImportData(*lang, *hasDeclValue)});
261 if (!Check(res.second, "Duplicated dynamic path '", key, "' for key '", key, "'")) {
262 return false;
263 }
264 }
265
266 return true;
267 }
268
269 template <class Collection, class Function>
ParseCollection(const JsonObject *config, Collection &out, const std::string &target, Function &&constructor)270 static bool ParseCollection(const JsonObject *config, Collection &out, const std::string &target,
271 Function &&constructor)
272 {
273 auto arr = config->GetValue<JsonObject::ArrayT>(target);
274 if (arr != nullptr) {
275 out = {};
276 if (!Check(!arr->empty(), "The '", target, "' list in config file is empty")) {
277 return false;
278 }
279
280 for (auto &i : *arr) {
281 out.emplace_back(constructor(*i.Get<JsonObject::StringT>()));
282 }
283 }
284
285 return true;
286 }
287
ReadConfig(const std::string &path)288 static std::optional<std::string> ReadConfig(const std::string &path)
289 {
290 std::ifstream inputStream(path);
291 if (!Check(!inputStream.fail(), "Failed to open file: ", path)) {
292 return {};
293 }
294
295 std::stringstream ss;
296 ss << inputStream.rdbuf();
297 return ss.str();
298 }
299
ParseRelDir(std::string &dst, const std::string &key, const JsonObject::JsonObjPointer *options, const std::string &configDir)300 static void ParseRelDir(std::string &dst, const std::string &key, const JsonObject::JsonObjPointer *options,
301 const std::string &configDir)
302 {
303 if (options != nullptr) {
304 auto path = options->get()->GetValue<JsonObject::StringT>(key);
305 dst = ((path != nullptr) ? *path : "");
306 }
307
308 dst = MakeAbsolute(dst, configDir);
309 }
310
Parse()311 bool ArkTsConfig::Parse()
312 {
313 static const std::string BASE_URL = "baseUrl";
314 static const std::string COMPILER_OPTIONS = "compilerOptions";
315 static const std::string EXCLUDE = "exclude";
316 static const std::string EXTENDS = "extends";
317 static const std::string FILES = "files";
318 static const std::string INCLUDE = "include";
319 static const std::string OUT_DIR = "outDir";
320 static const std::string ROOT_DIR = "rootDir";
321
322 ASSERT(!isParsed_);
323 isParsed_ = true;
324 auto arktsConfigDir = ParentPath(ark::os::GetAbsolutePath(configPath_));
325
326 // Read input
327 auto tsConfigSource = ReadConfig(configPath_);
328 if (!tsConfigSource) {
329 return false;
330 }
331
332 // Parse json
333 auto arktsConfig = std::make_unique<JsonObject>(*tsConfigSource);
334 if (!Check(arktsConfig->IsValid(), "ArkTsConfig is not valid json")) {
335 return false;
336 }
337
338 #ifdef ARKTSCONFIG_USE_FILESYSTEM
339 // Parse "extends"
340 auto extends = arktsConfig->GetValue<JsonObject::StringT>(EXTENDS);
341 if (extends != nullptr && !ParseExtends(*extends, arktsConfigDir)) {
342 return false;
343 }
344 #endif // ARKTSCONFIG_USE_FILESYSTEM
345
346 auto compilerOptions = arktsConfig->GetValue<JsonObject::JsonObjPointer>(COMPILER_OPTIONS);
347
348 // Parse "baseUrl", "outDir", "rootDir"
349 ParseRelDir(baseUrl_, BASE_URL, compilerOptions, arktsConfigDir);
350 ParseRelDir(outDir_, OUT_DIR, compilerOptions, arktsConfigDir);
351 ParseRelDir(rootDir_, ROOT_DIR, compilerOptions, arktsConfigDir);
352
353 // Parse "paths"
354 if (!ParsePaths(compilerOptions, paths_, baseUrl_) || !ParseDynamicPaths(compilerOptions, dynamicPaths_)) {
355 return false;
356 }
357
358 // Parse "files"
359 auto concatPath = [&arktsConfigDir](const auto &val) { return MakeAbsolute(val, arktsConfigDir); };
360 if (!ParseCollection(arktsConfig.get(), files_, FILES, concatPath)) {
361 return false;
362 }
363
364 #ifdef ARKTSCONFIG_USE_FILESYSTEM
365 // Parse "include" and "exclude"
366 auto consPattern = [&arktsConfigDir](const auto &val) { return Pattern {val, arktsConfigDir}; };
367 return ParseCollection(arktsConfig.get(), include_, INCLUDE, consPattern) &&
368 ParseCollection(arktsConfig.get(), exclude_, EXCLUDE, consPattern);
369 #else
370 return true;
371 #endif // ARKTSCONFIG_USE_FILESYSTEM
372 }
373
Inherit(const ArkTsConfig &base)374 void ArkTsConfig::Inherit(const ArkTsConfig &base)
375 {
376 baseUrl_ = base.baseUrl_;
377 outDir_ = base.outDir_;
378 rootDir_ = base.rootDir_;
379 paths_ = base.paths_;
380 files_ = base.files_;
381 #ifdef ARKTSCONFIG_USE_FILESYSTEM
382 include_ = base.include_;
383 exclude_ = base.exclude_;
384 #endif // ARKTSCONFIG_USE_FILESYSTEM
385 }
386
387 // Remove '/' and '*' from the end of path
TrimPath(const std::string &path)388 static std::string TrimPath(const std::string &path)
389 {
390 std::string trimmedPath = path;
391 while (!trimmedPath.empty() && (trimmedPath.back() == '*' || trimmedPath.back() == '/')) {
392 trimmedPath.pop_back();
393 }
394 return trimmedPath;
395 }
396
ResolvePath(const std::string &path) const397 std::optional<std::string> ArkTsConfig::ResolvePath(const std::string &path) const
398 {
399 for (const auto &[alias, paths] : paths_) {
400 auto trimmedAlias = TrimPath(alias);
401 size_t pos = path.rfind(trimmedAlias, 0);
402 if (pos == 0) {
403 std::string resolved = path;
404 // NOTE(ivagin): arktsconfig contains array of paths for each prefix, for now just get first one
405 std::string newPrefix = TrimPath(paths[0]);
406 resolved.replace(pos, trimmedAlias.length(), newPrefix);
407 return resolved;
408 }
409 }
410 return std::nullopt;
411 }
412
413 #ifdef ARKTSCONFIG_USE_FILESYSTEM
MatchExcludes(const fs::path &path, const std::vector<ArkTsConfig::Pattern> &excludes)414 static bool MatchExcludes(const fs::path &path, const std::vector<ArkTsConfig::Pattern> &excludes)
415 {
416 for (auto &e : excludes) {
417 if (e.Match(path.string())) {
418 return true;
419 }
420 }
421 return false;
422 }
423
GetSourceList(const std::shared_ptr<ArkTsConfig> &arktsConfig)424 static std::vector<fs::path> GetSourceList(const std::shared_ptr<ArkTsConfig> &arktsConfig)
425 {
426 auto includes = arktsConfig->Include();
427 auto excludes = arktsConfig->Exclude();
428 auto files = arktsConfig->Files();
429
430 // If "files" and "includes" are empty - include everything from tsconfig root
431 auto configDir = fs::absolute(fs::path(arktsConfig->ConfigPath())).parent_path();
432 if (files.empty() && includes.empty()) {
433 includes = {ArkTsConfig::Pattern("**/*", configDir.string())};
434 }
435 // If outDir in not default add it into exclude
436 if (!fs::equivalent(arktsConfig->OutDir(), configDir)) {
437 excludes.emplace_back("**/*", arktsConfig->OutDir());
438 }
439
440 // Collect "files"
441 std::vector<fs::path> sourceList;
442 for (auto &f : files) {
443 if (!Check(fs::exists(f) && fs::path(f).has_filename(), "No such file: ", f)) {
444 return {};
445 }
446
447 sourceList.emplace_back(f);
448 }
449
450 // Collect "include"
451 // TSC traverses folders for sources starting from 'include' rather than from 'rootDir', so we do the same
452 for (auto &include : includes) {
453 auto traverseRoot = fs::path(include.GetSearchRoot());
454 if (!fs::exists(traverseRoot)) {
455 continue;
456 }
457 if (!fs::is_directory(traverseRoot)) {
458 if (include.Match(traverseRoot.string()) && !MatchExcludes(traverseRoot, excludes)) {
459 sourceList.emplace_back(traverseRoot);
460 }
461 continue;
462 }
463 for (const auto &dirEntry : fs::recursive_directory_iterator(traverseRoot)) {
464 if (include.Match(dirEntry.path().string()) && !MatchExcludes(dirEntry, excludes)) {
465 sourceList.emplace_back(dirEntry);
466 }
467 }
468 }
469 return sourceList;
470 }
471
472 // Analogue of 'std::filesystem::relative()'
473 // Example: Relative("/a/b/c", "/a/b") returns "c"
Relative(const fs::path &src, const fs::path &base)474 static fs::path Relative(const fs::path &src, const fs::path &base)
475 {
476 fs::path tmpPath = src;
477 fs::path relPath;
478 while (!fs::equivalent(tmpPath, base)) {
479 relPath = relPath.empty() ? tmpPath.filename() : tmpPath.filename() / relPath;
480 if (tmpPath == tmpPath.parent_path()) {
481 return fs::path();
482 }
483 tmpPath = tmpPath.parent_path();
484 }
485 return relPath;
486 }
487
488 // Compute path to destination file and create subfolders
ComputeDestination(const fs::path &src, const fs::path &rootDir, const fs::path &outDir)489 static fs::path ComputeDestination(const fs::path &src, const fs::path &rootDir, const fs::path &outDir)
490 {
491 auto rel = Relative(src, rootDir);
492 if (!Check(!rel.empty(), rootDir, " is not root directory for ", src)) {
493 return {};
494 }
495
496 auto dst = outDir / rel;
497 fs::create_directories(dst.parent_path());
498 return dst.replace_extension("abc");
499 }
500
FindProjectSources(const std::shared_ptr<ArkTsConfig> &arktsConfig)501 std::vector<std::pair<std::string, std::string>> FindProjectSources(const std::shared_ptr<ArkTsConfig> &arktsConfig)
502 {
503 auto sourceFiles = GetSourceList(arktsConfig);
504 std::vector<std::pair<std::string, std::string>> compilationList;
505 for (auto &src : sourceFiles) {
506 auto dst = ComputeDestination(src, arktsConfig->RootDir(), arktsConfig->OutDir());
507 if (!Check(!dst.empty(), "Invalid destination file")) {
508 return {};
509 }
510
511 compilationList.emplace_back(src.string(), dst.string());
512 }
513
514 return compilationList;
515 }
516 #else
FindProjectSources( [[maybe_unused]] const std::shared_ptr<ArkTsConfig> &arkts_config)517 std::vector<std::pair<std::string, std::string>> FindProjectSources(
518 [[maybe_unused]] const std::shared_ptr<ArkTsConfig> &arkts_config)
519 {
520 ASSERT(false);
521 return {};
522 }
523 #endif // ARKTSCONFIG_USE_FILESYSTEM
524
525 } // namespace ark::es2panda
526