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