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>
35namespace fs = std::filesystem;
36#elif __has_include(<experimental/filesystem>)
37#include <experimental/filesystem>
38namespace fs = std::experimental::filesystem;
39#endif
40#endif
41
42namespace ark::es2panda {
43
44template <class... Ts>
45static 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
55static 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
64std::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
73std::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
83static 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
90ArkTsConfig::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
95bool ArkTsConfig::Pattern::IsPattern() const
96{
97    return (value_.find('*') != std::string::npos) || (value_.find('?') != std::string::npos);
98}
99
100std::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
114bool 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
138static 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
152bool 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
169static 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
180template <class PathsMap>
181static 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
216template <class PathsMap>
217static 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
269template <class Collection, class Function>
270static 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
288static 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
300static 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
311bool 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
374void 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
388static 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
397std::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
414static 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
424static 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"
474static 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
489static 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
501std::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
517std::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