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