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