1 /**
2 * Copyright (c) 2021-2025 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 "ETSchecker.h"
17
18 namespace ark::es2panda::checker {
19
CheckerContext(Checker * checker,CheckerStatus newStatus,ETSObjectType const * containingClass,Signature * containingSignature)20 CheckerContext::CheckerContext(Checker *checker, CheckerStatus newStatus, ETSObjectType const *containingClass,
21 Signature *containingSignature)
22 : parent_(checker),
23 status_(newStatus),
24 capturedVars_(parent_->Allocator()->Adapter()),
25 smartCasts_(parent_->Allocator()->Adapter()),
26 containingClass_(containingClass),
27 containingSignature_(containingSignature),
28 testSmartCasts_(parent_->Allocator()->Adapter()),
29 breakSmartCasts_(parent_->Allocator()->Adapter())
30 {
31 }
32
SetSmartCast(varbinder::Variable const * const variable,checker::Type * const smartType)33 void CheckerContext::SetSmartCast(varbinder::Variable const *const variable, checker::Type *const smartType) noexcept
34 {
35 // Just block captured and modified variables here instead of finding all their usage occurrences.
36 if (!variable->HasFlag(varbinder::VariableFlags::CAPTURED_MODIFIED)) {
37 smartCasts_.insert_or_assign(variable, smartType);
38 }
39 }
40
CloneTestSmartCasts(bool const clearData)41 SmartCastTypes CheckerContext::CloneTestSmartCasts(bool const clearData) noexcept
42 {
43 if (testSmartCasts_.empty()) {
44 return std::nullopt;
45 }
46
47 SmartCastTestArray smartCasts {};
48 smartCasts.reserve(testSmartCasts_.size());
49
50 for (auto [variable, types] : testSmartCasts_) {
51 if (types.first != nullptr || types.second != nullptr) {
52 smartCasts.emplace_back(variable, types.first, types.second);
53 }
54 }
55
56 if (clearData) {
57 ClearTestSmartCasts();
58 }
59
60 return std::make_optional(smartCasts);
61 }
62
CloneSmartCasts(bool const clearData)63 SmartCastArray CheckerContext::CloneSmartCasts(bool const clearData) noexcept
64 {
65 SmartCastArray smartCasts {};
66
67 if (!smartCasts_.empty()) {
68 smartCasts.reserve(smartCasts_.size());
69
70 for (auto const [variable, type] : smartCasts_) {
71 smartCasts.emplace_back(variable, type);
72 }
73 }
74
75 if (clearData) {
76 ClearSmartCasts();
77 }
78
79 return smartCasts;
80 }
81
RestoreSmartCasts(SmartCastArray const & otherSmartCasts)82 void CheckerContext::RestoreSmartCasts(SmartCastArray const &otherSmartCasts)
83 {
84 smartCasts_.clear();
85 if (!otherSmartCasts.empty()) {
86 for (auto [variable, type] : otherSmartCasts) {
87 smartCasts_.emplace(variable, type);
88 }
89 }
90 }
91
RemoveSmartCasts(SmartCastArray const & otherSmartCasts)92 void CheckerContext::RemoveSmartCasts(SmartCastArray const &otherSmartCasts) noexcept
93 {
94 if (!smartCasts_.empty()) {
95 auto it = smartCasts_.begin();
96 while (it != smartCasts_.end()) {
97 if (std::find_if(otherSmartCasts.begin(), otherSmartCasts.end(), [&it](auto const &item) -> bool {
98 return item.first == it->first;
99 }) == otherSmartCasts.end()) {
100 it = smartCasts_.erase(it);
101 } else {
102 ++it;
103 }
104 }
105 }
106 }
107
108 // Auxiliary private method returning combined type (if types differ) or 'nullptr' if types are identical
109 // and no smart cast change is required.
CombineTypes(checker::Type * const typeOne,checker::Type * const typeTwo) const110 checker::Type *CheckerContext::CombineTypes(checker::Type *const typeOne, checker::Type *const typeTwo) const noexcept
111 {
112 ES2PANDA_ASSERT(typeOne != nullptr && typeTwo != nullptr);
113 auto *const checker = parent_->AsETSChecker();
114
115 if (checker->Relation()->IsIdenticalTo(typeOne, typeTwo)) {
116 return nullptr;
117 }
118
119 return checker->CreateETSUnionType({typeOne, typeTwo});
120 }
121
CombineSmartCasts(SmartCastArray const & otherSmartCasts)122 void CheckerContext::CombineSmartCasts(SmartCastArray const &otherSmartCasts)
123 {
124 auto *const checker = parent_->AsETSChecker();
125
126 for (auto [variable, type] : otherSmartCasts) {
127 auto const it = smartCasts_.find(variable);
128 if (it == smartCasts_.end()) {
129 continue;
130 }
131 // Smart cast presents in both sets
132 if (auto *const smartType = CombineTypes(it->second, type); smartType != nullptr) {
133 // Remove it or set to new combined value
134 if (checker->Relation()->IsIdenticalTo(it->first->TsType(), smartType)) {
135 smartCasts_.erase(it);
136 } else {
137 it->second = smartType;
138 }
139 }
140 }
141
142 // Remove smart casts that don't present in the other set.
143 RemoveSmartCasts(otherSmartCasts);
144 }
145
146 // Second return value shows if the 'IN_LOOP' flag should be cleared on exit from the loop (case of nested loops).
EnterLoop(const ir::LoopStatement & loop,const SmartCastTypes loopConditionSmartCasts)147 std::pair<SmartCastArray, bool> CheckerContext::EnterLoop(const ir::LoopStatement &loop,
148 const SmartCastTypes loopConditionSmartCasts) noexcept
149 {
150 bool const clearFlag = !IsInLoop();
151 if (clearFlag) {
152 status_ |= CheckerStatus::IN_LOOP;
153 }
154
155 auto smartCasts = CloneSmartCasts();
156
157 ReassignedVariableMap changedVariables {};
158 if (loop.IsWhileStatement()) {
159 // In 'while' loops, we only invalidate smart casts for reassigned variables in the body. If a variable is
160 // being reassigned in the test condition, it'll have that type until it's reassigned in the body
161 loop.AsWhileStatement()->Body()->Iterate(
162 [this, &changedVariables](ir::AstNode *childNode) { CheckAssignments(childNode, changedVariables); });
163 } else {
164 // Handling 'for' and 'do-while' loops is a bit different, as the above statement on 'while' loops doesn't hold
165 // here. Later we'll need to implement these checks too.
166 loop.Iterate(
167 [this, &changedVariables](ir::AstNode *childNode) { CheckAssignments(childNode, changedVariables); });
168 }
169
170 const auto variableIsConstrainedInPrecondition = [&loopConditionSmartCasts](const varbinder::Variable *var) {
171 if (!loopConditionSmartCasts.has_value()) {
172 return false;
173 }
174
175 return std::find_if(loopConditionSmartCasts->begin(), loopConditionSmartCasts->end(),
176 [&var](const SmartCastTuple &smartCast) { return std::get<0>(smartCast) == var; }) !=
177 loopConditionSmartCasts->end();
178 };
179
180 if (!changedVariables.empty()) {
181 for (const auto &[variable, isAccessedAfterReassign] : changedVariables) {
182 // Two cases to invalidate a smart cast:
183 // - when a variable is used in the body after reassignment
184 // - when a variable is reassigned, and the precondition of the loop does not restrict its type
185 // Currently it allows us to keep the smart types in some cases. It's good enough for now, as a
186 // complete solution would require CFG/DFG, and smart casts will be rewritten with those in the future.
187 if (isAccessedAfterReassign || !(variableIsConstrainedInPrecondition(variable))) {
188 smartCasts_.erase(variable);
189 }
190 }
191 }
192
193 return {std::move(smartCasts), clearFlag};
194 }
195
ExitLoop(SmartCastArray & prevSmartCasts,bool const clearFlag,ir::LoopStatement * loopStatement)196 void CheckerContext::ExitLoop(SmartCastArray &prevSmartCasts, bool const clearFlag,
197 ir::LoopStatement *loopStatement) noexcept
198 {
199 if (clearFlag) {
200 status_ &= ~CheckerStatus::IN_LOOP;
201 }
202
203 if (!breakSmartCasts_.empty()) {
204 auto it = breakSmartCasts_.begin();
205
206 while (it != breakSmartCasts_.end()) {
207 if (it->first != loopStatement) {
208 ++it;
209 } else {
210 CombineSmartCasts(it->second);
211 it = breakSmartCasts_.erase(it);
212 }
213 }
214 }
215
216 // Now we don't process smart casts inside the loops correctly, thus just combine them on exit from the loop.
217 CombineSmartCasts(prevSmartCasts);
218 }
219
CheckAssignments(ir::AstNode const * node,ReassignedVariableMap & changedVariables) const220 void CheckerContext::CheckAssignments(ir::AstNode const *node, ReassignedVariableMap &changedVariables) const noexcept
221 {
222 if (node == nullptr) { // Just in case!
223 return;
224 }
225
226 if (!node->IsAssignmentExpression()) {
227 // If the node is an identifier, check if it was reassigned before
228 if (node->IsIdentifier() && changedVariables.count(node->AsIdentifier()->Variable()) != 0) {
229 changedVariables[node->AsIdentifier()->Variable()] = true;
230 }
231
232 node->Iterate(
233 [this, &changedVariables](ir::AstNode *childNode) { CheckAssignments(childNode, changedVariables); });
234 return;
235 }
236
237 auto const *assignment = node->AsAssignmentExpression();
238 if (assignment->Left()->IsIdentifier()) {
239 auto const *const ident = assignment->Left()->AsIdentifier();
240
241 auto const *variable = ident->Variable();
242 if (variable == nullptr) {
243 // NOTE: we're interesting in the local variables ONLY!
244 variable = parent_->AsETSChecker()->FindVariableInFunctionScope(ident->Name());
245 }
246
247 if (variable != nullptr) {
248 changedVariables.insert({variable, false});
249 }
250 }
251
252 assignment->Right()->Iterate(
253 [this, &changedVariables](ir::AstNode *childNode) { CheckAssignments(childNode, changedVariables); });
254 }
255
CheckTryBlock(ir::BlockStatement const & tryBlock)256 SmartCastArray CheckerContext::CheckTryBlock(ir::BlockStatement const &tryBlock) noexcept
257 {
258 ReassignedVariableMap changedVariables {};
259 tryBlock.Iterate(
260 [this, &changedVariables](ir::AstNode *childNode) { CheckAssignments(childNode, changedVariables); });
261
262 SmartCastArray smartCasts {};
263 if (!smartCasts_.empty()) {
264 smartCasts.reserve(smartCasts_.size());
265
266 for (const auto &[variable, type] : smartCasts_) {
267 if (changedVariables.count(variable) == 0) {
268 smartCasts.emplace_back(variable, type);
269 }
270 }
271 }
272
273 return smartCasts;
274 }
275
276 // Check that the expression is a part of logical OR/AND or unary negation operators chain
277 // (other cases are not interested)
IsInValidChain(ir::AstNode const * parent)278 bool CheckerContext::IsInValidChain(ir::AstNode const *parent) noexcept
279 {
280 while (parent != nullptr) {
281 if (parent->IsBinaryExpression()) {
282 auto const operation = parent->AsBinaryExpression()->OperatorType();
283 if (operation != lexer::TokenType::PUNCTUATOR_LOGICAL_OR &&
284 operation != lexer::TokenType::PUNCTUATOR_LOGICAL_AND) {
285 return false;
286 }
287 } else if (parent->IsUnaryExpression()) {
288 if (parent->AsUnaryExpression()->OperatorType() != lexer::TokenType::PUNCTUATOR_EXCLAMATION_MARK) {
289 return false;
290 }
291 } else if (parent->IsExpression()) {
292 return parent->IsConditionalExpression();
293 } else {
294 return true;
295 }
296 parent = parent->Parent();
297 }
298 return parent != nullptr;
299 }
300
CheckIdentifierSmartCastCondition(ir::Identifier const * const identifier)301 void CheckerContext::CheckIdentifierSmartCastCondition(ir::Identifier const *const identifier) noexcept
302 {
303 if (!IsInTestExpression()) {
304 return;
305 }
306
307 auto const *const variable = identifier->Variable();
308 ES2PANDA_ASSERT(variable != nullptr);
309
310 // Smart cast for extended conditional check can be applied only to the variables of reference types.
311 if (auto const *const variableType = variable->TsType(); !variableType->IsETSReferenceType()) {
312 return;
313 }
314
315 if (!IsInValidChain(identifier->Parent())) {
316 return;
317 }
318
319 if (identifier->TsType()->PossiblyETSNullish()) {
320 ES2PANDA_ASSERT(testCondition_.variable == nullptr);
321 testCondition_ = {variable, parent_->AsETSChecker()->GlobalETSNullType(), true, false};
322 }
323 }
324
CheckUnarySmartCastCondition(ir::UnaryExpression const * const unaryExpression)325 void CheckerContext::CheckUnarySmartCastCondition(ir::UnaryExpression const *const unaryExpression) noexcept
326 {
327 if (!IsInTestExpression() || unaryExpression->OperatorType() != lexer::TokenType::PUNCTUATOR_EXCLAMATION_MARK) {
328 return;
329 }
330
331 auto const *const argument = unaryExpression->Argument();
332 if (argument == nullptr || (!argument->IsIdentifier() && !argument->IsBinaryExpression())) {
333 return;
334 }
335
336 if (!IsInValidChain(unaryExpression->Parent())) {
337 return;
338 }
339
340 if (testCondition_.variable != nullptr) {
341 testCondition_.negate = !testCondition_.negate;
342 }
343 }
344
CheckBinarySmartCastCondition(ir::BinaryExpression * const binaryExpression)345 void CheckerContext::CheckBinarySmartCastCondition(ir::BinaryExpression *const binaryExpression) noexcept
346 {
347 if (!IsInTestExpression() || !IsInValidChain(binaryExpression->Parent())) {
348 return;
349 }
350
351 if (auto const operatorType = binaryExpression->OperatorType(); operatorType == lexer::TokenType::KEYW_INSTANCEOF) {
352 if (binaryExpression->Left()->IsIdentifier()) {
353 if (binaryExpression->Right()->TsType() == nullptr) {
354 return;
355 }
356 // NOTE(pantos) Issue with generics
357 // eg
358 // class C <T> { ... }
359 // let x = new C<...>
360 // if (x instanceof C && x.fld instanceof ...
361 const auto variable = binaryExpression->Left()->AsIdentifier()->Variable();
362 auto type = binaryExpression->Right()->TsType();
363 auto smartIt = smartCasts_.find(variable);
364 // NOTE(pantos) Handle union types e.g. C<A>|C<B>|...
365 if (type->HasTypeFlag(TypeFlag::GENERIC) && smartIt != smartCasts_.end() && type->IsETSObjectType() &&
366 type->AsETSObjectType()->IsSameBasedGeneric(type->AsETSObjectType()->GetRelation(), smartIt->second)) {
367 // Replace generic type with instantiated one, e.g. C<T> with C<A>
368 type = smartIt->second;
369 }
370 ES2PANDA_ASSERT(testCondition_.variable == nullptr);
371 testCondition_ = {variable, type};
372 }
373 } else if (operatorType == lexer::TokenType::PUNCTUATOR_STRICT_EQUAL ||
374 operatorType == lexer::TokenType::PUNCTUATOR_NOT_STRICT_EQUAL ||
375 operatorType == lexer::TokenType::PUNCTUATOR_EQUAL ||
376 operatorType == lexer::TokenType::PUNCTUATOR_NOT_EQUAL) {
377 CheckSmartCastEqualityCondition(binaryExpression);
378 }
379 }
380
381 // Extracted just to avoid large length and depth of method 'CheckBinarySmartCastCondition()'.
CheckSmartCastEqualityCondition(ir::BinaryExpression * const binaryExpression)382 void CheckerContext::CheckSmartCastEqualityCondition(ir::BinaryExpression *const binaryExpression) noexcept
383 {
384 varbinder::Variable const *variable = nullptr;
385 checker::Type *testedType = nullptr;
386 auto const operatorType = binaryExpression->OperatorType();
387
388 bool strict = operatorType == lexer::TokenType::PUNCTUATOR_NOT_STRICT_EQUAL ||
389 operatorType == lexer::TokenType::PUNCTUATOR_STRICT_EQUAL;
390
391 // extracted just to avoid extra nested level
392 auto const getTestedType = [&variable, &testedType, &strict](ir::Identifier const *const identifier,
393 ir::Expression *const expression) -> void {
394 ES2PANDA_ASSERT(identifier != nullptr && expression != nullptr);
395 variable = identifier->Variable();
396 if (expression->IsLiteral()) {
397 testedType = expression->TsType();
398 if (!expression->IsNullLiteral() && !expression->IsUndefinedLiteral()) {
399 strict = false;
400 }
401 }
402 };
403
404 if (binaryExpression->Left()->IsIdentifier()) {
405 getTestedType(binaryExpression->Left()->AsIdentifier(), binaryExpression->Right());
406 }
407
408 if (testedType == nullptr && binaryExpression->Right()->IsIdentifier()) {
409 getTestedType(binaryExpression->Right()->AsIdentifier(), binaryExpression->Left());
410 }
411
412 if (testedType != nullptr) {
413 bool const negate = operatorType == lexer::TokenType::PUNCTUATOR_NOT_STRICT_EQUAL ||
414 operatorType == lexer::TokenType::PUNCTUATOR_NOT_EQUAL;
415
416 if (testedType->DefinitelyETSNullish()) {
417 ES2PANDA_ASSERT(testCondition_.variable == nullptr);
418 testCondition_ = {variable, testedType, negate, strict};
419 }
420 }
421 }
422
ClearTestSmartCasts()423 void CheckerContext::ClearTestSmartCasts() noexcept
424 {
425 testCondition_ = {};
426 testSmartCasts_.clear();
427 operatorType_ = lexer::TokenType::EOS;
428 }
429
GetSmartCast(varbinder::Variable const * const variable) const430 checker::Type *CheckerContext::GetSmartCast(varbinder::Variable const *const variable) const noexcept
431 {
432 if (IsInTestExpression()) {
433 if (operatorType_ == lexer::TokenType::PUNCTUATOR_LOGICAL_AND) {
434 if (auto const it = testSmartCasts_.find(variable);
435 it != testSmartCasts_.end() && it->second.first != nullptr) {
436 return it->second.first;
437 }
438 } else if (operatorType_ == lexer::TokenType::PUNCTUATOR_LOGICAL_OR) {
439 if (auto const it = testSmartCasts_.find(variable);
440 it != testSmartCasts_.end() && it->second.second != nullptr) {
441 return it->second.second;
442 }
443 }
444 }
445
446 auto const it = smartCasts_.find(variable);
447 return it == smartCasts_.end() ? nullptr : it->second;
448 }
449
OnBreakStatement(ir::BreakStatement const * breakStatement)450 void CheckerContext::OnBreakStatement(ir::BreakStatement const *breakStatement)
451 {
452 if (breakStatement->Target() == nullptr) {
453 ES2PANDA_ASSERT(parent_->IsAnyError());
454 return;
455 }
456
457 ir::Statement const *targetStatement = breakStatement->Target()->AsStatement();
458 ES2PANDA_ASSERT(targetStatement != nullptr);
459 if (targetStatement->IsLabelledStatement()) {
460 targetStatement = targetStatement->AsLabelledStatement()->Body();
461 }
462 ES2PANDA_ASSERT(targetStatement != nullptr);
463
464 auto const inInnerScope = [targetStatement](varbinder::Scope const *scope, ir::AstNode const *parent) -> bool {
465 do {
466 parent = parent->Parent();
467 if (parent->IsScopeBearer() && parent->Scope() == scope) {
468 return true;
469 }
470 } while (parent != targetStatement);
471 return false;
472 };
473
474 status_ |= CheckerStatus::MEET_BREAK;
475
476 if (smartCasts_.empty()) {
477 return;
478 }
479
480 SmartCastArray smartCasts {};
481 smartCasts.reserve(smartCasts_.size());
482
483 for (auto const [variable, type] : smartCasts_) {
484 if (!inInnerScope(variable->AsLocalVariable()->GetScope(), breakStatement)) {
485 smartCasts.emplace_back(variable, type);
486 }
487 }
488
489 if (!smartCasts.empty()) {
490 AddBreakSmartCasts(targetStatement, std::move(smartCasts));
491 }
492
493 ClearSmartCasts();
494 }
495
AddBreakSmartCasts(ir::Statement const * targetStatement,SmartCastArray && smartCasts)496 void CheckerContext::AddBreakSmartCasts(ir::Statement const *targetStatement, SmartCastArray &&smartCasts)
497 {
498 breakSmartCasts_.emplace(targetStatement, std::move(smartCasts));
499 }
500
CombineBreakSmartCasts(ir::Statement const * targetStatement)501 void CheckerContext::CombineBreakSmartCasts(ir::Statement const *targetStatement)
502 {
503 ES2PANDA_ASSERT(smartCasts_.empty());
504
505 if (!breakSmartCasts_.empty()) {
506 bool firstCase = true;
507 auto it = breakSmartCasts_.begin();
508
509 while (it != breakSmartCasts_.end()) {
510 if (it->first != targetStatement) {
511 ++it;
512 continue;
513 }
514
515 if (firstCase) {
516 firstCase = false;
517 RestoreSmartCasts(it->second);
518 } else {
519 CombineSmartCasts(it->second);
520 }
521
522 it = breakSmartCasts_.erase(it);
523 }
524 }
525 }
526 } // namespace ark::es2panda::checker
527