diff --git a/src/ast/ast.h b/src/ast/ast.h index 8473f7fb67..f111660d95 100644 --- a/src/ast/ast.h +++ b/src/ast/ast.h @@ -1613,7 +1613,9 @@ enum AssignType { PRIVATE_METHOD, // obj.#key: #key is a private method PRIVATE_GETTER_ONLY, // obj.#key: #key only has a getter defined PRIVATE_SETTER_ONLY, // obj.#key: #key only has a setter defined - PRIVATE_GETTER_AND_SETTER // obj.#key: #key has both accessors defined + PRIVATE_GETTER_AND_SETTER, // obj.#key: #key has both accessors defined + PRIVATE_DEBUG_DYNAMIC, // obj.#key: #key is private that requries dynamic + // lookup in debug-evaluate. }; class Property final : public Expression { @@ -1650,6 +1652,9 @@ class Property final : public Expression { return PRIVATE_SETTER_ONLY; case VariableMode::kPrivateGetterAndSetter: return PRIVATE_GETTER_AND_SETTER; + case VariableMode::kDynamic: + // From debug-evaluate. + return PRIVATE_DEBUG_DYNAMIC; default: UNREACHABLE(); } diff --git a/src/ast/prettyprinter.cc b/src/ast/prettyprinter.cc index e0cb7da7af..5705ed9ca3 100644 --- a/src/ast/prettyprinter.cc +++ b/src/ast/prettyprinter.cc @@ -1367,6 +1367,10 @@ void AstPrinter::VisitProperty(Property* node) { PrintIndentedVisit("KEY", node->key()); break; } + case PRIVATE_DEBUG_DYNAMIC: { + PrintIndentedVisit("PRIVATE_DEBUG_DYNAMIC", node->key()); + break; + } case NON_PROPERTY: UNREACHABLE(); } diff --git a/src/ast/scopes.cc b/src/ast/scopes.cc index ad7014adb5..842a7dc717 100644 --- a/src/ast/scopes.cc +++ b/src/ast/scopes.cc @@ -2061,6 +2061,15 @@ Variable* Scope::NonLocal(const AstRawString* name, VariableMode mode) { return var; } +void Scope::ForceDynamicLookup(VariableProxy* proxy) { + // At the moment this is only used for looking up private names dynamically + // in debug-evaluate from top-level scope. + DCHECK(proxy->IsPrivateName()); + DCHECK(is_script_scope() || is_module_scope() || is_eval_scope()); + Variable* dynamic = NonLocal(proxy->raw_name(), VariableMode::kDynamic); + proxy->BindTo(dynamic); +} + // static template Variable* Scope::Lookup(VariableProxy* proxy, Scope* scope, @@ -3112,6 +3121,13 @@ void PrivateNameScopeIterator::AddUnresolvedPrivateName(VariableProxy* proxy) { // be new. DCHECK(!proxy->is_resolved()); DCHECK(proxy->IsPrivateName()); + + // Use dynamic lookup for top-level scopes in debug-evaluate. + if (Done()) { + start_scope_->ForceDynamicLookup(proxy); + return; + } + GetScope()->EnsureRareData()->unresolved_private_names.Add(proxy); // Any closure scope that contain uses of private names that skips over a // class scope due to heritage expressions need private name context chain diff --git a/src/ast/scopes.h b/src/ast/scopes.h index 3d06268564..cba189ba0e 100644 --- a/src/ast/scopes.h +++ b/src/ast/scopes.h @@ -637,6 +637,8 @@ class V8_EXPORT_PRIVATE Scope : public NON_EXPORTED_BASE(ZoneObject) { return nullptr; } + void ForceDynamicLookup(VariableProxy* proxy); + protected: explicit Scope(Zone* zone); diff --git a/src/common/message-template.h b/src/common/message-template.h index 29b821b66b..bca6ad8d80 100644 --- a/src/common/message-template.h +++ b/src/common/message-template.h @@ -13,6 +13,9 @@ namespace internal { #define MESSAGE_TEMPLATES(T) \ /* Error */ \ T(None, "") \ + T(ConflictingPrivateName, \ + "Operation is ambiguous because there are more than one private name" \ + "'%' on the object") \ T(CyclicProto, "Cyclic __proto__ value") \ T(Debugger, "Debugger: %") \ T(DebuggerLoading, "Error loading debugger") \ @@ -149,6 +152,7 @@ namespace internal { T(NonObjectAssertOption, "The 'assert' option must be an object") \ T(NonObjectInInstanceOfCheck, \ "Right-hand side of 'instanceof' is not an object") \ + T(NonObjectPrivateNameAccess, "Cannot access private name % from %") \ T(NonObjectPropertyLoad, "Cannot read properties of %") \ T(NonObjectPropertyLoadWithProperty, \ "Cannot read properties of % (reading '%')") \ diff --git a/src/interpreter/bytecode-generator.cc b/src/interpreter/bytecode-generator.cc index e8be6644cd..7b4103d172 100644 --- a/src/interpreter/bytecode-generator.cc +++ b/src/interpreter/bytecode-generator.cc @@ -3638,8 +3638,15 @@ void BytecodeGenerator::BuildVariableLoad(Variable* variable, feedback_index(slot), depth); break; } - default: + default: { + // Normally, private names should not be looked up dynamically, + // but we make an exception in debug-evaluate, in that case the + // lookup will be done in %SetPrivateMember() and %GetPrivateMember() + // calls, not here. + DCHECK(!variable->raw_name()->IsPrivateName()); builder()->LoadLookupSlot(variable->raw_name(), typeof_mode); + break; + } } break; } @@ -3944,6 +3951,14 @@ BytecodeGenerator::AssignmentLhsData::PrivateMethodOrAccessor( } // static BytecodeGenerator::AssignmentLhsData +BytecodeGenerator::AssignmentLhsData::PrivateDebugEvaluate(AssignType type, + Property* property, + Register object) { + return AssignmentLhsData(type, property, RegisterList(), object, Register(), + nullptr, nullptr); +} +// static +BytecodeGenerator::AssignmentLhsData BytecodeGenerator::AssignmentLhsData::KeyedSuperProperty( RegisterList super_property_args) { return AssignmentLhsData(KEYED_SUPER_PROPERTY, nullptr, super_property_args, @@ -3984,6 +3999,13 @@ BytecodeGenerator::AssignmentLhsData BytecodeGenerator::PrepareAssignmentLhs( return AssignmentLhsData::PrivateMethodOrAccessor(assign_type, property, object, key); } + case PRIVATE_DEBUG_DYNAMIC: { + AccumulatorPreservingScope scope(this, accumulator_preserving_mode); + Register object = VisitForRegisterValue(property->obj()); + // Do not visit the key here, instead we will look them up at run time. + return AssignmentLhsData::PrivateDebugEvaluate(assign_type, property, + object); + } case NAMED_SUPER_PROPERTY: { AccumulatorPreservingScope scope(this, accumulator_preserving_mode); RegisterList super_property_args = @@ -4560,6 +4582,16 @@ void BytecodeGenerator::BuildAssignment( } break; } + case PRIVATE_DEBUG_DYNAMIC: { + Register value = register_allocator()->NewRegister(); + builder()->StoreAccumulatorInRegister(value); + Property* property = lhs_data.expr()->AsProperty(); + BuildPrivateDebugDynamicSet(property, lhs_data.object(), value); + if (!execution_result()->IsEffect()) { + builder()->LoadAccumulatorWithRegister(value); + } + break; + } } } @@ -4631,6 +4663,11 @@ void BytecodeGenerator::VisitCompoundAssignment(CompoundAssignment* expr) { lhs_data.expr()->AsProperty()); break; } + case PRIVATE_DEBUG_DYNAMIC: { + Property* property = lhs_data.expr()->AsProperty(); + BuildPrivateDebugDynamicGet(property, lhs_data.object()); + break; + } } BinaryOperation* binop = expr->binary_operation(); @@ -5143,9 +5180,41 @@ void BytecodeGenerator::VisitPropertyLoad(Register obj, Property* property) { VisitForAccumulatorValue(property->key()); break; } + case PRIVATE_DEBUG_DYNAMIC: { + BuildPrivateDebugDynamicGet(property, obj); + break; + } } } +void BytecodeGenerator::BuildPrivateDebugDynamicGet(Property* property, + Register obj) { + RegisterAllocationScope scope(this); + RegisterList args = register_allocator()->NewRegisterList(2); + + Variable* private_name = property->key()->AsVariableProxy()->var(); + builder() + ->MoveRegister(obj, args[0]) + .LoadLiteral(private_name->raw_name()) + .StoreAccumulatorInRegister(args[1]) + .CallRuntime(Runtime::kGetPrivateMember, args); +} + +void BytecodeGenerator::BuildPrivateDebugDynamicSet(Property* property, + Register obj, + Register value) { + RegisterAllocationScope scope(this); + RegisterList args = register_allocator()->NewRegisterList(3); + + Variable* private_name = property->key()->AsVariableProxy()->var(); + builder() + ->MoveRegister(obj, args[0]) + .LoadLiteral(private_name->raw_name()) + .StoreAccumulatorInRegister(args[1]) + .MoveRegister(value, args[2]) + .CallRuntime(Runtime::kSetPrivateMember, args); +} + void BytecodeGenerator::BuildPrivateGetterAccess(Register object, Register accessor_pair) { RegisterAllocationScope scope(this); @@ -6081,6 +6150,11 @@ void BytecodeGenerator::VisitCountOperation(CountOperation* expr) { BuildPrivateGetterAccess(object, key); break; } + case PRIVATE_DEBUG_DYNAMIC: { + object = VisitForRegisterValue(property->obj()); + BuildPrivateDebugDynamicGet(property, object); + break; + } } // Save result for postfix expressions. @@ -6161,6 +6235,12 @@ void BytecodeGenerator::VisitCountOperation(CountOperation* expr) { } break; } + case PRIVATE_DEBUG_DYNAMIC: { + Register value = register_allocator()->NewRegister(); + builder()->StoreAccumulatorInRegister(value); + BuildPrivateDebugDynamicSet(property, object, value); + break; + } } // Restore old value for postfix expressions. diff --git a/src/interpreter/bytecode-generator.h b/src/interpreter/bytecode-generator.h index 63174d44fc..a114c0dfdf 100644 --- a/src/interpreter/bytecode-generator.h +++ b/src/interpreter/bytecode-generator.h @@ -102,17 +102,24 @@ class BytecodeGenerator final : public AstVisitor { Property* property, Register object, Register key); + static AssignmentLhsData PrivateDebugEvaluate(AssignType type, + Property* property, + Register object); static AssignmentLhsData NamedSuperProperty( RegisterList super_property_args); static AssignmentLhsData KeyedSuperProperty( RegisterList super_property_args); AssignType assign_type() const { return assign_type_; } - Expression* expr() const { - DCHECK(assign_type_ == NON_PROPERTY || assign_type_ == PRIVATE_METHOD || + bool is_private_assign_type() const { + return assign_type_ == PRIVATE_METHOD || assign_type_ == PRIVATE_GETTER_ONLY || assign_type_ == PRIVATE_SETTER_ONLY || - assign_type_ == PRIVATE_GETTER_AND_SETTER); + assign_type_ == PRIVATE_GETTER_AND_SETTER || + assign_type_ == PRIVATE_DEBUG_DYNAMIC; + } + Expression* expr() const { + DCHECK(assign_type_ == NON_PROPERTY || is_private_assign_type()); return expr_; } Expression* object_expr() const { @@ -121,17 +128,12 @@ class BytecodeGenerator final : public AstVisitor { } Register object() const { DCHECK(assign_type_ == NAMED_PROPERTY || assign_type_ == KEYED_PROPERTY || - assign_type_ == PRIVATE_METHOD || - assign_type_ == PRIVATE_GETTER_ONLY || - assign_type_ == PRIVATE_SETTER_ONLY || - assign_type_ == PRIVATE_GETTER_AND_SETTER); + is_private_assign_type()); return object_; } Register key() const { - DCHECK(assign_type_ == KEYED_PROPERTY || assign_type_ == PRIVATE_METHOD || - assign_type_ == PRIVATE_GETTER_ONLY || - assign_type_ == PRIVATE_SETTER_ONLY || - assign_type_ == PRIVATE_GETTER_AND_SETTER); + DCHECK((assign_type_ == KEYED_PROPERTY || is_private_assign_type()) && + assign_type_ != PRIVATE_DEBUG_DYNAMIC); return key_; } const AstRawString* name() const { @@ -332,6 +334,9 @@ class BytecodeGenerator final : public AstVisitor { void BuildPrivateGetterAccess(Register obj, Register access_pair); void BuildPrivateSetterAccess(Register obj, Register access_pair, Register value); + void BuildPrivateDebugDynamicGet(Property* property, Register obj); + void BuildPrivateDebugDynamicSet(Property* property, Register obj, + Register value); void BuildPrivateMethods(ClassLiteral* expr, bool is_static, Register home_object); void BuildClassProperty(ClassLiteral::Property* property); diff --git a/src/parsing/parser-base.h b/src/parsing/parser-base.h index 3431590611..af00e63679 100644 --- a/src/parsing/parser-base.h +++ b/src/parsing/parser-base.h @@ -305,6 +305,8 @@ class ParserBase { // The current Zone, which might be the main zone or a temporary Zone. Zone* zone() const { return zone_; } + V8_INLINE bool IsExtraordinaryPrivateNameAccessAllowed() const; + protected: friend class v8::internal::ExpressionScope>; friend class v8::internal::ExpressionParsingScope>; @@ -1759,6 +1761,39 @@ typename ParserBase::IdentifierT ParserBase::ParsePropertyName() { return impl()->EmptyIdentifierString(); } +template +bool ParserBase::IsExtraordinaryPrivateNameAccessAllowed() const { + if (flags().parsing_while_debugging() != ParsingWhileDebugging::kYes && + !flags().is_repl_mode()) { + return false; + } + Scope* current_scope = scope(); + while (current_scope != nullptr) { + switch (current_scope->scope_type()) { + case CLASS_SCOPE: + case CATCH_SCOPE: + case BLOCK_SCOPE: + case WITH_SCOPE: + case SHADOW_REALM_SCOPE: + return false; + // Top-level scopes. + case SCRIPT_SCOPE: + case MODULE_SCOPE: + return true; + // Top-level wrapper function scopes. + case FUNCTION_SCOPE: + return function_literal_id_ == kFunctionLiteralIdTopLevel; + // Used by debug-evaluate. If the outer scope is top-level, + // extraordinary private name access is allowed. + case EVAL_SCOPE: + current_scope = current_scope->outer_scope(); + DCHECK_NOT_NULL(current_scope); + break; + } + } + UNREACHABLE(); +} + template typename ParserBase::ExpressionT ParserBase::ParsePropertyOrPrivatePropertyName() { @@ -1780,7 +1815,10 @@ ParserBase::ParsePropertyOrPrivatePropertyName() { PrivateNameScopeIterator private_name_scope_iter(scope()); // Parse the identifier so that we can display it in the error message name = impl()->GetIdentifier(); - if (private_name_scope_iter.Done()) { + // In debug-evaluate, we relax the private name resolution to enable + // evaluation of obj.#member outside the class bodies in top-level scopes. + if (private_name_scope_iter.Done() && + !IsExtraordinaryPrivateNameAccessAllowed()) { impl()->ReportMessageAt(Scanner::Location(pos, pos + 1), MessageTemplate::kInvalidPrivateFieldResolution, impl()->GetRawNameFromIdentifier(name)); diff --git a/src/parsing/preparser.h b/src/parsing/preparser.h index 44ea20e919..ea3308fc32 100644 --- a/src/parsing/preparser.h +++ b/src/parsing/preparser.h @@ -1565,7 +1565,7 @@ class PreParser : public ParserBase { return PreParserExpression::StringLiteral(); } - PreParserExpression ExpressionFromPrivateName( + V8_INLINE PreParserExpression ExpressionFromPrivateName( PrivateNameScopeIterator* private_name_scope, const PreParserIdentifier& name, int start_position) { VariableProxy* proxy = factory()->ast_node_factory()->NewVariableProxy( diff --git a/src/runtime/runtime-object.cc b/src/runtime/runtime-object.cc index f110e5e649..325b0aff4f 100644 --- a/src/runtime/runtime-object.cc +++ b/src/runtime/runtime-object.cc @@ -1491,6 +1491,231 @@ RUNTIME_FUNCTION(Runtime_GetOwnPropertyDescriptorObject) { return *desc.ToPropertyDescriptorObject(isolate); } +enum class PrivateMemberType { + kPrivateField, + kPrivateAccessor, + kPrivateMethod, +}; + +struct PrivateMember { + PrivateMemberType type; + // It's the class constructor for static methods/accessors, + // the brand symbol for instance methods/accessors, + // and the private name symbol for fields. + Handle brand_or_field_symbol; + Handle value; +}; + +namespace { +void CollectPrivateMethodsAndAccessorsFromContext( + Isolate* isolate, Handle context, Handle desc, + Handle brand, IsStaticFlag is_static_flag, + std::vector* results) { + Handle scope_info(context->scope_info(), isolate); + VariableLookupResult lookup_result; + int context_index = scope_info->ContextSlotIndex(desc, &lookup_result); + if (context_index == -1 || + !IsPrivateMethodOrAccessorVariableMode(lookup_result.mode) || + lookup_result.is_static_flag != is_static_flag) { + return; + } + + Handle slot_value(context->get(context_index), isolate); + DCHECK_IMPLIES(lookup_result.mode == VariableMode::kPrivateMethod, + slot_value->IsJSFunction()); + DCHECK_IMPLIES(lookup_result.mode != VariableMode::kPrivateMethod, + slot_value->IsAccessorPair()); + results->push_back({ + lookup_result.mode == VariableMode::kPrivateMethod + ? PrivateMemberType::kPrivateMethod + : PrivateMemberType::kPrivateAccessor, + brand, + slot_value, + }); +} + +Maybe CollectPrivateMembersFromReceiver( + Isolate* isolate, Handle receiver, Handle desc, + std::vector* results) { + PropertyFilter key_filter = + static_cast(PropertyFilter::PRIVATE_NAMES_ONLY); + Handle keys; + ASSIGN_RETURN_ON_EXCEPTION_VALUE( + isolate, keys, + KeyAccumulator::GetKeys(isolate, receiver, KeyCollectionMode::kOwnOnly, + key_filter, GetKeysConversion::kConvertToString), + Nothing()); + + if (receiver->IsJSFunction()) { + Handle func(JSFunction::cast(*receiver), isolate); + Handle shared(func->shared(), isolate); + if (shared->is_class_constructor() && + shared->has_static_private_methods_or_accessors()) { + Handle recevier_context(JSFunction::cast(*receiver).context(), + isolate); + CollectPrivateMethodsAndAccessorsFromContext( + isolate, recevier_context, desc, func, IsStaticFlag::kStatic, + results); + } + } + + for (int i = 0; i < keys->length(); ++i) { + Handle obj_key(keys->get(i), isolate); + Handle symbol(Symbol::cast(*obj_key), isolate); + CHECK(symbol->is_private_name()); + Handle value; + ASSIGN_RETURN_ON_EXCEPTION_VALUE( + isolate, value, Object::GetProperty(isolate, receiver, symbol), + Nothing()); + + if (symbol->is_private_brand()) { + Handle value_context(Context::cast(*value), isolate); + CollectPrivateMethodsAndAccessorsFromContext( + isolate, value_context, desc, symbol, IsStaticFlag::kNotStatic, + results); + } else { + Handle symbol_desc(String::cast(symbol->description()), isolate); + if (symbol_desc->Equals(*desc)) { + results->push_back({ + PrivateMemberType::kPrivateField, + symbol, + value, + }); + } + } + } + + return Just(true); +} + +Maybe FindPrivateMembersFromReceiver(Isolate* isolate, + Handle receiver, + Handle desc, + MessageTemplate not_found_message, + PrivateMember* result) { + std::vector results; + MAYBE_RETURN( + CollectPrivateMembersFromReceiver(isolate, receiver, desc, &results), + Nothing()); + + if (results.size() == 0) { + THROW_NEW_ERROR_RETURN_VALUE(isolate, NewError(not_found_message, desc), + Nothing()); + } else if (results.size() > 1) { + THROW_NEW_ERROR_RETURN_VALUE( + isolate, NewError(MessageTemplate::kConflictingPrivateName, desc), + Nothing()); + } + + *result = results[0]; + return Just(true); +} +} // namespace + +MaybeHandle Runtime::GetPrivateMember(Isolate* isolate, + Handle receiver, + Handle desc) { + PrivateMember result; + MAYBE_RETURN_NULL(FindPrivateMembersFromReceiver( + isolate, receiver, desc, MessageTemplate::kInvalidPrivateMemberRead, + &result)); + + switch (result.type) { + case PrivateMemberType::kPrivateField: + case PrivateMemberType::kPrivateMethod: { + return result.value; + } + case PrivateMemberType::kPrivateAccessor: { + // The accessors are collected from the contexts, so there is no need to + // perform brand checks. + Handle pair = Handle::cast(result.value); + if (pair->getter().IsNull()) { + THROW_NEW_ERROR( + isolate, + NewError(MessageTemplate::kInvalidPrivateGetterAccess, desc), + Object); + } + DCHECK(pair->getter().IsJSFunction()); + Handle getter(JSFunction::cast(pair->getter()), isolate); + return Execution::Call(isolate, getter, receiver, 0, nullptr); + } + } +} + +MaybeHandle Runtime::SetPrivateMember(Isolate* isolate, + Handle receiver, + Handle desc, + Handle value) { + PrivateMember result; + MAYBE_RETURN_NULL(FindPrivateMembersFromReceiver( + isolate, receiver, desc, MessageTemplate::kInvalidPrivateMemberRead, + &result)); + + switch (result.type) { + case PrivateMemberType::kPrivateField: { + Handle symbol = + Handle::cast(result.brand_or_field_symbol); + return Object::SetProperty(isolate, receiver, symbol, value, + StoreOrigin::kMaybeKeyed); + } + case PrivateMemberType::kPrivateMethod: { + THROW_NEW_ERROR( + isolate, NewError(MessageTemplate::kInvalidPrivateMethodWrite, desc), + Object); + } + case PrivateMemberType::kPrivateAccessor: { + // The accessors are collected from the contexts, so there is no need to + // perform brand checks. + Handle pair = Handle::cast(result.value); + if (pair->setter().IsNull()) { + THROW_NEW_ERROR( + isolate, + NewError(MessageTemplate::kInvalidPrivateSetterAccess, desc), + Object); + } + DCHECK(pair->setter().IsJSFunction()); + Handle argv[] = {value}; + Handle setter(JSFunction::cast(pair->setter()), isolate); + return Execution::Call(isolate, setter, receiver, arraysize(argv), argv); + } + } +} + +RUNTIME_FUNCTION(Runtime_GetPrivateMember) { + HandleScope scope(isolate); + // TODO(chromium:1381806) support specifying scopes, or selecting the right + // one from the conflicting names. + DCHECK_EQ(args.length(), 2); + Handle receiver = args.at(0); + Handle desc = args.at(1); + if (receiver->IsNullOrUndefined(isolate)) { + THROW_NEW_ERROR_RETURN_FAILURE( + isolate, NewTypeError(MessageTemplate::kNonObjectPrivateNameAccess, + desc, receiver)); + } + RETURN_RESULT_OR_FAILURE( + isolate, Runtime::GetPrivateMember( + isolate, Handle::cast(receiver), desc)); +} + +RUNTIME_FUNCTION(Runtime_SetPrivateMember) { + HandleScope scope(isolate); + // TODO(chromium:1381806) support specifying scopes, or selecting the right + // one from the conflicting names. + DCHECK_EQ(args.length(), 3); + Handle receiver = args.at(0); + Handle desc = args.at(1); + if (receiver->IsNullOrUndefined(isolate)) { + THROW_NEW_ERROR_RETURN_FAILURE( + isolate, NewTypeError(MessageTemplate::kNonObjectPrivateNameAccess, + desc, receiver)); + } + Handle value = args.at(2); + RETURN_RESULT_OR_FAILURE( + isolate, Runtime::SetPrivateMember( + isolate, Handle::cast(receiver), desc, value)); +} + RUNTIME_FUNCTION(Runtime_LoadPrivateSetter) { HandleScope scope(isolate); DCHECK_EQ(args.length(), 1); diff --git a/src/runtime/runtime.h b/src/runtime/runtime.h index 668a9fdf63..d73f337891 100644 --- a/src/runtime/runtime.h +++ b/src/runtime/runtime.h @@ -319,6 +319,7 @@ namespace internal { F(GetOwnPropertyDescriptor, 2, 1) \ F(GetOwnPropertyDescriptorObject, 2, 1) \ F(GetOwnPropertyKeys, 2, 1) \ + F(GetPrivateMember, 2, 1) \ F(GetProperty, -1 /* [2, 3] */, 1) \ F(HasFastPackedElements, 1, 1) \ F(HasInPrototypeChain, 2, 1) \ @@ -360,6 +361,7 @@ namespace internal { F(ToObject, 1, 1) \ F(ToString, 1, 1) \ F(TryMigrateInstance, 1, 1) \ + F(SetPrivateMember, 3, 1) \ F(SwissTableAdd, 4, 1) \ F(SwissTableAllocate, 1, 1) \ F(SwissTableDelete, 2, 1) \ @@ -878,6 +880,29 @@ class Runtime : public AllStatic { Handle receiver = Handle(), bool* is_found = nullptr); + // Look up for a private member with a name matching "desc" and return its + // value. "desc" should be a #-prefixed string, in the case of private fields, + // it should match the description of the private name symbol. Throw an error + // if the found private member is an accessor without a getter, or there is no + // matching private member, or there are more than one matching private member + // (which would be ambiguous). If the found private member is an accessor with + // a getter, the getter will be called to set the value. + V8_EXPORT_PRIVATE V8_WARN_UNUSED_RESULT static MaybeHandle + GetPrivateMember(Isolate* isolate, Handle receiver, + Handle desc); + + // Look up for a private member with a name matching "desc" and set it to + // "value". "desc" should be a #-prefixed string, in the case of private + // fields, it should match the description of the private name symbol. Throw + // an error if the found private member is a private method, or an accessor + // without a setter, or there is no matching private member, or there are more + // than one matching private member (which would be ambiguous). + // If the found private member is an accessor with a setter, the setter will + // be called to set the value. + V8_EXPORT_PRIVATE V8_WARN_UNUSED_RESULT static MaybeHandle + SetPrivateMember(Isolate* isolate, Handle receiver, + Handle desc, Handle value); + V8_WARN_UNUSED_RESULT static MaybeHandle HasProperty( Isolate* isolate, Handle object, Handle key); diff --git a/test/inspector/debugger/evaluate-on-call-frame-private-class-member-conflict-expected.txt b/test/inspector/debugger/evaluate-on-call-frame-private-class-member-conflict-expected.txt new file mode 100644 index 0000000000..5b406d0a4d --- /dev/null +++ b/test/inspector/debugger/evaluate-on-call-frame-private-class-member-conflict-expected.txt @@ -0,0 +1,55 @@ +Evaluate conflicting private class member out of class scope in Debugger.evaluateOnCallFrame() + +class Klass { + #name = "string"; +} +class ClassWithField extends Klass { + #name = "child"; +} +class ClassWithMethod extends Klass { + #name() {} +} +class ClassWithAccessor extends Klass { + get #name() {} + set #name(val) {} +} +class StaticClass extends Klass { + static #name = "child"; +} +debugger; + +Running test: evaluatePrivateMembers +Debugger.evaluateOnCallFrame: `(new ClassWithField).#name` +{ + className : Error + description : Error: Operation is ambiguous because there are more than one private name'#name' on the object at eval (eval at (:18:1), :1:2) at :18:1 + objectId : + subtype : error + type : object +} +Debugger.evaluateOnCallFrame: `(new ClassWithMethod).#name` +{ + className : Error + description : Error: Operation is ambiguous because there are more than one private name'#name' on the object at eval (eval at (:18:1), :1:2) at :18:1 + objectId : + subtype : error + type : object +} +Debugger.evaluateOnCallFrame: `(new ClassWithAccessor).#name` +{ + className : Error + description : Error: Operation is ambiguous because there are more than one private name'#name' on the object at eval (eval at (:18:1), :1:2) at :18:1 + objectId : + subtype : error + type : object +} +Debugger.evaluateOnCallFrame: `StaticClass.#name` +{ + type : string + value : child +} +Debugger.evaluateOnCallFrame: `(new StaticClass).#name` +{ + type : string + value : string +} diff --git a/test/inspector/debugger/evaluate-on-call-frame-private-class-member-conflict.js b/test/inspector/debugger/evaluate-on-call-frame-private-class-member-conflict.js new file mode 100644 index 0000000000..8dae59021c --- /dev/null +++ b/test/inspector/debugger/evaluate-on-call-frame-private-class-member-conflict.js @@ -0,0 +1,12 @@ +// Copyright 2022 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +utils.load('test/inspector/private-class-member-inspector-test.js'); + +const options = { + type: 'private-conflicting-member', + testRuntime: false, + message: `Evaluate conflicting private class member out of class scope in Debugger.evaluateOnCallFrame()` +}; +PrivateClassMemberInspectorTest.runTest(InspectorTest, options); diff --git a/test/inspector/debugger/evaluate-on-call-frame-private-class-member-expected.txt b/test/inspector/debugger/evaluate-on-call-frame-private-class-member-expected.txt new file mode 100644 index 0000000000..651306c9cd --- /dev/null +++ b/test/inspector/debugger/evaluate-on-call-frame-private-class-member-expected.txt @@ -0,0 +1,231 @@ +Evaluate private class member out of class scope in Debugger.evaluateOnCallFrame() + +class Klass { + #field = "string"; + get #getterOnly() { return "getterOnly"; } + set #setterOnly(val) { this.#field = "setterOnlyCalled"; } + get #accessor() { return this.#field } + set #accessor(val) { this.#field = val; } + #method() { return "method"; } +} +const obj = new Klass(); +debugger; + +Running test: evaluatePrivateMembers +Checking private fields +Debugger.evaluateOnCallFrame: `obj.#field` +{ + type : string + value : string +} +Debugger.evaluateOnCallFrame: `obj.#field = 1` +{ + description : 1 + type : number + value : 1 +} +Debugger.evaluateOnCallFrame: `obj.#field` +{ + description : 1 + type : number + value : 1 +} +Debugger.evaluateOnCallFrame: `obj.#field++` +{ + description : 1 + type : number + value : 1 +} +Debugger.evaluateOnCallFrame: `obj.#field` +{ + description : 2 + type : number + value : 2 +} +Debugger.evaluateOnCallFrame: `++obj.#field` +{ + description : 3 + type : number + value : 3 +} +Debugger.evaluateOnCallFrame: `obj.#field` +{ + description : 3 + type : number + value : 3 +} +Debugger.evaluateOnCallFrame: `obj.#field -= 3` +{ + description : 0 + type : number + value : 0 +} +Debugger.evaluateOnCallFrame: `obj.#field` +{ + description : 0 + type : number + value : 0 +} +Checking private getter-only accessors +Debugger.evaluateOnCallFrame: `obj.#getterOnly` +{ + type : string + value : getterOnly +} +Debugger.evaluateOnCallFrame: `obj.#getterOnly = 1` +{ + className : Error + description : Error: '#getterOnly' was defined without a setter at eval (eval at (:11:1), :1:17) at :11:1 + objectId : + subtype : error + type : object +} +Debugger.evaluateOnCallFrame: `obj.#getterOnly++` +{ + className : Error + description : Error: '#getterOnly' was defined without a setter at eval (eval at (:11:1), :1:16) at :11:1 + objectId : + subtype : error + type : object +} +Debugger.evaluateOnCallFrame: `obj.#getterOnly -= 3` +{ + className : Error + description : Error: '#getterOnly' was defined without a setter at eval (eval at (:11:1), :1:17) at :11:1 + objectId : + subtype : error + type : object +} +Debugger.evaluateOnCallFrame: `obj.#getterOnly` +{ + type : string + value : getterOnly +} +Checking private setter-only accessors +Debugger.evaluateOnCallFrame: `obj.#setterOnly` +{ + className : Error + description : Error: '#setterOnly' was defined without a getter at eval (eval at (:11:1), :1:1) at :11:1 + objectId : + subtype : error + type : object +} +Debugger.evaluateOnCallFrame: `obj.#setterOnly = 1` +{ + description : 1 + type : number + value : 1 +} +Debugger.evaluateOnCallFrame: `obj.#setterOnly++` +{ + className : Error + description : Error: '#setterOnly' was defined without a getter at eval (eval at (:11:1), :1:1) at :11:1 + objectId : + subtype : error + type : object +} +Debugger.evaluateOnCallFrame: `obj.#setterOnly -= 3` +{ + className : Error + description : Error: '#setterOnly' was defined without a getter at eval (eval at (:11:1), :1:1) at :11:1 + objectId : + subtype : error + type : object +} +Debugger.evaluateOnCallFrame: `obj.#field` +{ + type : string + value : setterOnlyCalled +} +Checking private accessors +Debugger.evaluateOnCallFrame: `obj.#accessor` +{ + type : string + value : setterOnlyCalled +} +Debugger.evaluateOnCallFrame: `obj.#accessor = 1` +{ + description : 1 + type : number + value : 1 +} +Debugger.evaluateOnCallFrame: `obj.#field` +{ + description : 1 + type : number + value : 1 +} +Debugger.evaluateOnCallFrame: `obj.#accessor++` +{ + description : 1 + type : number + value : 1 +} +Debugger.evaluateOnCallFrame: `obj.#field` +{ + description : 2 + type : number + value : 2 +} +Debugger.evaluateOnCallFrame: `++obj.#accessor` +{ + type : undefined +} +Debugger.evaluateOnCallFrame: `obj.#field` +{ + description : 3 + type : number + value : 3 +} +Debugger.evaluateOnCallFrame: `obj.#accessor -= 3` +{ + description : 0 + type : number + value : 0 +} +Debugger.evaluateOnCallFrame: `obj.#field` +{ + description : 0 + type : number + value : 0 +} +Checking private methods +Debugger.evaluateOnCallFrame: `obj.#method` +{ + className : Function + description : #method() { return "method"; } + objectId : + type : function +} +Debugger.evaluateOnCallFrame: `obj.#method = 1` +{ + className : Error + description : Error: Private method '#method' is not writable at eval (eval at (:11:1), :1:13) at :11:1 + objectId : + subtype : error + type : object +} +Debugger.evaluateOnCallFrame: `obj.#method++` +{ + className : Error + description : Error: Private method '#method' is not writable at eval (eval at (:11:1), :1:12) at :11:1 + objectId : + subtype : error + type : object +} +Debugger.evaluateOnCallFrame: `++obj.#method` +{ + className : Error + description : Error: Private method '#method' is not writable at eval (eval at (:11:1), :1:7) at :11:1 + objectId : + subtype : error + type : object +} +Debugger.evaluateOnCallFrame: `obj.#method -= 3` +{ + className : Error + description : Error: Private method '#method' is not writable at eval (eval at (:11:1), :1:13) at :11:1 + objectId : + subtype : error + type : object +} diff --git a/test/inspector/debugger/evaluate-on-call-frame-private-class-member-in-module-expected.txt b/test/inspector/debugger/evaluate-on-call-frame-private-class-member-in-module-expected.txt new file mode 100644 index 0000000000..dd0cad227a --- /dev/null +++ b/test/inspector/debugger/evaluate-on-call-frame-private-class-member-in-module-expected.txt @@ -0,0 +1,16 @@ +Evaluate private class member out of class scope in Debugger.evaluateOnCallFrame() in module + +class Klass { + #field = 1; +} +const obj = new Klass; +debugger; + + +Running test: evaluatePrivateMembers +Debugger.evaluateOnCallFrame: `obj.#field` +{ + description : 1 + type : number + value : 1 +} diff --git a/test/inspector/debugger/evaluate-on-call-frame-private-class-member-in-module.js b/test/inspector/debugger/evaluate-on-call-frame-private-class-member-in-module.js new file mode 100644 index 0000000000..98b86f12bf --- /dev/null +++ b/test/inspector/debugger/evaluate-on-call-frame-private-class-member-in-module.js @@ -0,0 +1,32 @@ +// Copyright 2022 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +const { contextGroup, Protocol } = InspectorTest.start( + 'Evaluate private class member out of class scope in Debugger.evaluateOnCallFrame() in module' +); + +Protocol.Debugger.enable(); +const source = ` +class Klass { + #field = 1; +} +const obj = new Klass; +debugger; +`; + +InspectorTest.log(source); +contextGroup.addModule(source, 'module'); + +InspectorTest.runAsyncTestSuite([async function evaluatePrivateMembers() { + const { params: { callFrames } } = await Protocol.Debugger.oncePaused(); + const frame = callFrames[0]; + const expression = 'obj.#field'; + InspectorTest.log(`Debugger.evaluateOnCallFrame: \`${expression}\``); + const { result: { result } } = + await Protocol.Debugger.evaluateOnCallFrame({ + callFrameId: frame.callFrameId, + expression + }); + InspectorTest.logMessage(result); +}]); diff --git a/test/inspector/debugger/evaluate-on-call-frame-private-class-member-static-expected.txt b/test/inspector/debugger/evaluate-on-call-frame-private-class-member-static-expected.txt new file mode 100644 index 0000000000..28122d3999 --- /dev/null +++ b/test/inspector/debugger/evaluate-on-call-frame-private-class-member-static-expected.txt @@ -0,0 +1,231 @@ +Evaluate static private class member out of class scope in Debugger.evaluateOnCallFrame() + +class Klass { + static #field = "string"; + static get #getterOnly() { return "getterOnly"; } + static set #setterOnly(val) { this.#field = "setterOnlyCalled"; } + static get #accessor() { return this.#field } + static set #accessor(val) { this.#field = val; } + static #method() { return "method"; } +} +const obj = new Klass(); +debugger; + +Running test: evaluatePrivateMembers +Checking private fields +Debugger.evaluateOnCallFrame: `Klass.#field` +{ + type : string + value : string +} +Debugger.evaluateOnCallFrame: `Klass.#field = 1` +{ + description : 1 + type : number + value : 1 +} +Debugger.evaluateOnCallFrame: `Klass.#field` +{ + description : 1 + type : number + value : 1 +} +Debugger.evaluateOnCallFrame: `Klass.#field++` +{ + description : 1 + type : number + value : 1 +} +Debugger.evaluateOnCallFrame: `Klass.#field` +{ + description : 2 + type : number + value : 2 +} +Debugger.evaluateOnCallFrame: `++Klass.#field` +{ + description : 3 + type : number + value : 3 +} +Debugger.evaluateOnCallFrame: `Klass.#field` +{ + description : 3 + type : number + value : 3 +} +Debugger.evaluateOnCallFrame: `Klass.#field -= 3` +{ + description : 0 + type : number + value : 0 +} +Debugger.evaluateOnCallFrame: `Klass.#field` +{ + description : 0 + type : number + value : 0 +} +Checking private getter-only accessors +Debugger.evaluateOnCallFrame: `Klass.#getterOnly` +{ + type : string + value : getterOnly +} +Debugger.evaluateOnCallFrame: `Klass.#getterOnly = 1` +{ + className : Error + description : Error: '#getterOnly' was defined without a setter at eval (eval at (:11:1), :1:19) at :11:1 + objectId : + subtype : error + type : object +} +Debugger.evaluateOnCallFrame: `Klass.#getterOnly++` +{ + className : Error + description : Error: '#getterOnly' was defined without a setter at eval (eval at (:11:1), :1:18) at :11:1 + objectId : + subtype : error + type : object +} +Debugger.evaluateOnCallFrame: `Klass.#getterOnly -= 3` +{ + className : Error + description : Error: '#getterOnly' was defined without a setter at eval (eval at (:11:1), :1:19) at :11:1 + objectId : + subtype : error + type : object +} +Debugger.evaluateOnCallFrame: `Klass.#getterOnly` +{ + type : string + value : getterOnly +} +Checking private setter-only accessors +Debugger.evaluateOnCallFrame: `Klass.#setterOnly` +{ + className : Error + description : Error: '#setterOnly' was defined without a getter at eval (eval at (:11:1), :1:1) at :11:1 + objectId : + subtype : error + type : object +} +Debugger.evaluateOnCallFrame: `Klass.#setterOnly = 1` +{ + description : 1 + type : number + value : 1 +} +Debugger.evaluateOnCallFrame: `Klass.#setterOnly++` +{ + className : Error + description : Error: '#setterOnly' was defined without a getter at eval (eval at (:11:1), :1:1) at :11:1 + objectId : + subtype : error + type : object +} +Debugger.evaluateOnCallFrame: `Klass.#setterOnly -= 3` +{ + className : Error + description : Error: '#setterOnly' was defined without a getter at eval (eval at (:11:1), :1:1) at :11:1 + objectId : + subtype : error + type : object +} +Debugger.evaluateOnCallFrame: `Klass.#field` +{ + type : string + value : setterOnlyCalled +} +Checking private accessors +Debugger.evaluateOnCallFrame: `Klass.#accessor` +{ + type : string + value : setterOnlyCalled +} +Debugger.evaluateOnCallFrame: `Klass.#accessor = 1` +{ + description : 1 + type : number + value : 1 +} +Debugger.evaluateOnCallFrame: `Klass.#field` +{ + description : 1 + type : number + value : 1 +} +Debugger.evaluateOnCallFrame: `Klass.#accessor++` +{ + description : 1 + type : number + value : 1 +} +Debugger.evaluateOnCallFrame: `Klass.#field` +{ + description : 2 + type : number + value : 2 +} +Debugger.evaluateOnCallFrame: `++Klass.#accessor` +{ + type : undefined +} +Debugger.evaluateOnCallFrame: `Klass.#field` +{ + description : 3 + type : number + value : 3 +} +Debugger.evaluateOnCallFrame: `Klass.#accessor -= 3` +{ + description : 0 + type : number + value : 0 +} +Debugger.evaluateOnCallFrame: `Klass.#field` +{ + description : 0 + type : number + value : 0 +} +Checking private methods +Debugger.evaluateOnCallFrame: `Klass.#method` +{ + className : Function + description : #method() { return "method"; } + objectId : + type : function +} +Debugger.evaluateOnCallFrame: `Klass.#method = 1` +{ + className : Error + description : Error: Private method '#method' is not writable at eval (eval at (:11:1), :1:15) at :11:1 + objectId : + subtype : error + type : object +} +Debugger.evaluateOnCallFrame: `Klass.#method++` +{ + className : Error + description : Error: Private method '#method' is not writable at eval (eval at (:11:1), :1:14) at :11:1 + objectId : + subtype : error + type : object +} +Debugger.evaluateOnCallFrame: `++Klass.#method` +{ + className : Error + description : Error: Private method '#method' is not writable at eval (eval at (:11:1), :1:9) at :11:1 + objectId : + subtype : error + type : object +} +Debugger.evaluateOnCallFrame: `Klass.#method -= 3` +{ + className : Error + description : Error: Private method '#method' is not writable at eval (eval at (:11:1), :1:15) at :11:1 + objectId : + subtype : error + type : object +} diff --git a/test/inspector/debugger/evaluate-on-call-frame-private-class-member-static.js b/test/inspector/debugger/evaluate-on-call-frame-private-class-member-static.js new file mode 100644 index 0000000000..d01e88de5b --- /dev/null +++ b/test/inspector/debugger/evaluate-on-call-frame-private-class-member-static.js @@ -0,0 +1,12 @@ +// Copyright 2022 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +utils.load('test/inspector/private-class-member-inspector-test.js'); + +const options = { + type: 'private-static-member', + testRuntime: false, + message: `Evaluate static private class member out of class scope in Debugger.evaluateOnCallFrame()` +}; +PrivateClassMemberInspectorTest.runTest(InspectorTest, options); diff --git a/test/inspector/debugger/evaluate-on-call-frame-private-class-member-super-expected.txt b/test/inspector/debugger/evaluate-on-call-frame-private-class-member-super-expected.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/inspector/debugger/evaluate-on-call-frame-private-class-member.js b/test/inspector/debugger/evaluate-on-call-frame-private-class-member.js new file mode 100644 index 0000000000..667877920e --- /dev/null +++ b/test/inspector/debugger/evaluate-on-call-frame-private-class-member.js @@ -0,0 +1,12 @@ +// Copyright 2022 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +utils.load('test/inspector/private-class-member-inspector-test.js'); + +const options = { + type: 'private-instance-member', + testRuntime: false, + message: `Evaluate private class member out of class scope in Debugger.evaluateOnCallFrame()` +}; +PrivateClassMemberInspectorTest.runTest(InspectorTest, options); diff --git a/test/inspector/private-class-member-inspector-test.js b/test/inspector/private-class-member-inspector-test.js new file mode 100644 index 0000000000..7fc336693d --- /dev/null +++ b/test/inspector/private-class-member-inspector-test.js @@ -0,0 +1,167 @@ +// Copyright 2022 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +PrivateClassMemberInspectorTest = {}; + +function getTestReceiver(type) { + return type === 'private-instance-member' ? 'obj' : 'Klass'; +} + +function getSetupScript({ type, testRuntime }) { + const pause = testRuntime ? '' : 'debugger;'; + if (type === 'private-instance-member' || type === 'private-static-member') { + const isStatic = type === 'private-static-member'; + const prefix = isStatic ? 'static' : ''; + return ` +class Klass { + ${prefix} #field = "string"; + ${prefix} get #getterOnly() { return "getterOnly"; } + ${prefix} set #setterOnly(val) { this.#field = "setterOnlyCalled"; } + ${prefix} get #accessor() { return this.#field } + ${prefix} set #accessor(val) { this.#field = val; } + ${prefix} #method() { return "method"; } +} +const obj = new Klass(); +${pause}`; + } + + if (type !== 'private-conflicting-member') { + throw new Error('unknown test type'); + } + + return ` +class Klass { + #name = "string"; +} +class ClassWithField extends Klass { + #name = "child"; +} +class ClassWithMethod extends Klass { + #name() {} +} +class ClassWithAccessor extends Klass { + get #name() {} + set #name(val) {} +} +class StaticClass extends Klass { + static #name = "child"; +} +${pause}`; +} + +async function testAllPrivateMembers(type, runAndLog) { + const receiver = getTestReceiver(type); + InspectorTest.log('Checking private fields'); + await runAndLog(`${receiver}.#field`); + await runAndLog(`${receiver}.#field = 1`); + await runAndLog(`${receiver}.#field`); + await runAndLog(`${receiver}.#field++`); + await runAndLog(`${receiver}.#field`); + await runAndLog(`++${receiver}.#field`); + await runAndLog(`${receiver}.#field`); + await runAndLog(`${receiver}.#field -= 3`); + await runAndLog(`${receiver}.#field`); + + InspectorTest.log('Checking private getter-only accessors'); + await runAndLog(`${receiver}.#getterOnly`); + await runAndLog(`${receiver}.#getterOnly = 1`); + await runAndLog(`${receiver}.#getterOnly++`); + await runAndLog(`${receiver}.#getterOnly -= 3`); + await runAndLog(`${receiver}.#getterOnly`); + + InspectorTest.log('Checking private setter-only accessors'); + await runAndLog(`${receiver}.#setterOnly`); + await runAndLog(`${receiver}.#setterOnly = 1`); + await runAndLog(`${receiver}.#setterOnly++`); + await runAndLog(`${receiver}.#setterOnly -= 3`); + await runAndLog(`${receiver}.#field`); + + InspectorTest.log('Checking private accessors'); + await runAndLog(`${receiver}.#accessor`); + await runAndLog(`${receiver}.#accessor = 1`); + await runAndLog(`${receiver}.#field`); + await runAndLog(`${receiver}.#accessor++`); + await runAndLog(`${receiver}.#field`); + await runAndLog(`++${receiver}.#accessor`); + await runAndLog(`${receiver}.#field`); + await runAndLog(`${receiver}.#accessor -= 3`); + await runAndLog(`${receiver}.#field`); + + InspectorTest.log('Checking private methods'); + await runAndLog(`${receiver}.#method`); + await runAndLog(`${receiver}.#method = 1`); + await runAndLog(`${receiver}.#method++`); + await runAndLog(`++${receiver}.#method`); + await runAndLog(`${receiver}.#method -= 3`); +} + +async function testConflictingPrivateMembers(runAndLog) { + await runAndLog(`(new ClassWithField).#name`); + await runAndLog(`(new ClassWithMethod).#name`); + await runAndLog(`(new ClassWithAccessor).#name`); + await runAndLog(`StaticClass.#name`); + await runAndLog(`(new StaticClass).#name`); +} + +async function runPrivateClassMemberTest(Protocol, { type, testRuntime }) { + let runAndLog; + + if (testRuntime) { + runAndLog = async function runAndLog(expression) { + InspectorTest.log(`Runtime.evaluate: \`${expression}\``); + const { result: { result } } = + await Protocol.Runtime.evaluate({ expression, replMode: true }); + InspectorTest.logMessage(result); + } + } else { + const { params: { callFrames } } = await Protocol.Debugger.oncePaused(); + const frame = callFrames[0]; + + runAndLog = async function runAndLog(expression) { + InspectorTest.log(`Debugger.evaluateOnCallFrame: \`${expression}\``); + const { result: { result } } = + await Protocol.Debugger.evaluateOnCallFrame({ + callFrameId: frame.callFrameId, + expression + }); + InspectorTest.logMessage(result); + } + } + + switch (type) { + case 'private-instance-member': + case 'private-static-member': { + await testAllPrivateMembers(type, runAndLog); + break; + } + case 'private-conflicting-member': { + await testConflictingPrivateMembers(runAndLog); + break; + } + default: + throw new Error('unknown test type'); + } + await Protocol.Debugger.resume(); +} + +PrivateClassMemberInspectorTest.runTest = function (InspectorTest, options) { + const { contextGroup, Protocol } = InspectorTest.start(options.message); + + if (options.testRuntime) { + Protocol.Runtime.enable(); + } else { + Protocol.Debugger.enable(); + } + const source = getSetupScript(options); + InspectorTest.log(source); + if (options.module) { + contextGroup.addModule(source, 'module'); + } else { + contextGroup.addScript(source); + } + + InspectorTest.runAsyncTestSuite([async function evaluatePrivateMembers() { + await runPrivateClassMemberTest(Protocol, options); + }]); +} diff --git a/test/inspector/runtime/evaluate-private-class-member-conflict-expected.txt b/test/inspector/runtime/evaluate-private-class-member-conflict-expected.txt new file mode 100644 index 0000000000..5b92d1c6b4 --- /dev/null +++ b/test/inspector/runtime/evaluate-private-class-member-conflict-expected.txt @@ -0,0 +1,55 @@ +Evaluate conflicting private class member out of class scope in Runtime.evaluate() + +class Klass { + #name = "string"; +} +class ClassWithField extends Klass { + #name = "child"; +} +class ClassWithMethod extends Klass { + #name() {} +} +class ClassWithAccessor extends Klass { + get #name() {} + set #name(val) {} +} +class StaticClass extends Klass { + static #name = "child"; +} + + +Running test: evaluatePrivateMembers +Runtime.evaluate: `(new ClassWithField).#name` +{ + className : Error + description : Error: Operation is ambiguous because there are more than one private name'#name' on the object at :1:2 + objectId : + subtype : error + type : object +} +Runtime.evaluate: `(new ClassWithMethod).#name` +{ + className : Error + description : Error: Operation is ambiguous because there are more than one private name'#name' on the object at :1:2 + objectId : + subtype : error + type : object +} +Runtime.evaluate: `(new ClassWithAccessor).#name` +{ + className : Error + description : Error: Operation is ambiguous because there are more than one private name'#name' on the object at :1:2 + objectId : + subtype : error + type : object +} +Runtime.evaluate: `StaticClass.#name` +{ + type : string + value : child +} +Runtime.evaluate: `(new StaticClass).#name` +{ + type : string + value : string +} diff --git a/test/inspector/runtime/evaluate-private-class-member-conflict.js b/test/inspector/runtime/evaluate-private-class-member-conflict.js new file mode 100644 index 0000000000..95cc6237c0 --- /dev/null +++ b/test/inspector/runtime/evaluate-private-class-member-conflict.js @@ -0,0 +1,12 @@ +// Copyright 2022 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +utils.load('test/inspector/private-class-member-inspector-test.js'); + +const options = { + type: 'private-conflicting-member', + testRuntime: true, + message: `Evaluate conflicting private class member out of class scope in Runtime.evaluate()` +}; +PrivateClassMemberInspectorTest.runTest(InspectorTest, options); diff --git a/test/inspector/runtime/evaluate-private-class-member-expected.txt b/test/inspector/runtime/evaluate-private-class-member-expected.txt new file mode 100644 index 0000000000..9a81f57cf7 --- /dev/null +++ b/test/inspector/runtime/evaluate-private-class-member-expected.txt @@ -0,0 +1,231 @@ +Evaluate private class member out of class scope in Runtime.evaluate() + +class Klass { + #field = "string"; + get #getterOnly() { return "getterOnly"; } + set #setterOnly(val) { this.#field = "setterOnlyCalled"; } + get #accessor() { return this.#field } + set #accessor(val) { this.#field = val; } + #method() { return "method"; } +} +const obj = new Klass(); + + +Running test: evaluatePrivateMembers +Checking private fields +Runtime.evaluate: `obj.#field` +{ + type : string + value : string +} +Runtime.evaluate: `obj.#field = 1` +{ + description : 1 + type : number + value : 1 +} +Runtime.evaluate: `obj.#field` +{ + description : 1 + type : number + value : 1 +} +Runtime.evaluate: `obj.#field++` +{ + description : 1 + type : number + value : 1 +} +Runtime.evaluate: `obj.#field` +{ + description : 2 + type : number + value : 2 +} +Runtime.evaluate: `++obj.#field` +{ + description : 3 + type : number + value : 3 +} +Runtime.evaluate: `obj.#field` +{ + description : 3 + type : number + value : 3 +} +Runtime.evaluate: `obj.#field -= 3` +{ + description : 0 + type : number + value : 0 +} +Runtime.evaluate: `obj.#field` +{ + description : 0 + type : number + value : 0 +} +Checking private getter-only accessors +Runtime.evaluate: `obj.#getterOnly` +{ + type : string + value : getterOnly +} +Runtime.evaluate: `obj.#getterOnly = 1` +{ + className : Error + description : Error: '#getterOnly' was defined without a setter at :1:17 + objectId : + subtype : error + type : object +} +Runtime.evaluate: `obj.#getterOnly++` +{ + className : Error + description : Error: '#getterOnly' was defined without a setter at :1:16 + objectId : + subtype : error + type : object +} +Runtime.evaluate: `obj.#getterOnly -= 3` +{ + className : Error + description : Error: '#getterOnly' was defined without a setter at :1:17 + objectId : + subtype : error + type : object +} +Runtime.evaluate: `obj.#getterOnly` +{ + type : string + value : getterOnly +} +Checking private setter-only accessors +Runtime.evaluate: `obj.#setterOnly` +{ + className : Error + description : Error: '#setterOnly' was defined without a getter at :1:1 + objectId : + subtype : error + type : object +} +Runtime.evaluate: `obj.#setterOnly = 1` +{ + description : 1 + type : number + value : 1 +} +Runtime.evaluate: `obj.#setterOnly++` +{ + className : Error + description : Error: '#setterOnly' was defined without a getter at :1:1 + objectId : + subtype : error + type : object +} +Runtime.evaluate: `obj.#setterOnly -= 3` +{ + className : Error + description : Error: '#setterOnly' was defined without a getter at :1:1 + objectId : + subtype : error + type : object +} +Runtime.evaluate: `obj.#field` +{ + type : string + value : setterOnlyCalled +} +Checking private accessors +Runtime.evaluate: `obj.#accessor` +{ + type : string + value : setterOnlyCalled +} +Runtime.evaluate: `obj.#accessor = 1` +{ + description : 1 + type : number + value : 1 +} +Runtime.evaluate: `obj.#field` +{ + description : 1 + type : number + value : 1 +} +Runtime.evaluate: `obj.#accessor++` +{ + description : 1 + type : number + value : 1 +} +Runtime.evaluate: `obj.#field` +{ + description : 2 + type : number + value : 2 +} +Runtime.evaluate: `++obj.#accessor` +{ + type : undefined +} +Runtime.evaluate: `obj.#field` +{ + description : 3 + type : number + value : 3 +} +Runtime.evaluate: `obj.#accessor -= 3` +{ + description : 0 + type : number + value : 0 +} +Runtime.evaluate: `obj.#field` +{ + description : 0 + type : number + value : 0 +} +Checking private methods +Runtime.evaluate: `obj.#method` +{ + className : Function + description : #method() { return "method"; } + objectId : + type : function +} +Runtime.evaluate: `obj.#method = 1` +{ + className : Error + description : Error: Private method '#method' is not writable at :1:13 + objectId : + subtype : error + type : object +} +Runtime.evaluate: `obj.#method++` +{ + className : Error + description : Error: Private method '#method' is not writable at :1:12 + objectId : + subtype : error + type : object +} +Runtime.evaluate: `++obj.#method` +{ + className : Error + description : Error: Private method '#method' is not writable at :1:7 + objectId : + subtype : error + type : object +} +Runtime.evaluate: `obj.#method -= 3` +{ + className : Error + description : Error: Private method '#method' is not writable at :1:13 + objectId : + subtype : error + type : object +} diff --git a/test/inspector/runtime/evaluate-private-class-member-static-expected.txt b/test/inspector/runtime/evaluate-private-class-member-static-expected.txt new file mode 100644 index 0000000000..d52f4960fd --- /dev/null +++ b/test/inspector/runtime/evaluate-private-class-member-static-expected.txt @@ -0,0 +1,231 @@ +Evaluate static private class member out of class scope in Runtime.evaluate() + +class Klass { + static #field = "string"; + static get #getterOnly() { return "getterOnly"; } + static set #setterOnly(val) { this.#field = "setterOnlyCalled"; } + static get #accessor() { return this.#field } + static set #accessor(val) { this.#field = val; } + static #method() { return "method"; } +} +const obj = new Klass(); + + +Running test: evaluatePrivateMembers +Checking private fields +Runtime.evaluate: `Klass.#field` +{ + type : string + value : string +} +Runtime.evaluate: `Klass.#field = 1` +{ + description : 1 + type : number + value : 1 +} +Runtime.evaluate: `Klass.#field` +{ + description : 1 + type : number + value : 1 +} +Runtime.evaluate: `Klass.#field++` +{ + description : 1 + type : number + value : 1 +} +Runtime.evaluate: `Klass.#field` +{ + description : 2 + type : number + value : 2 +} +Runtime.evaluate: `++Klass.#field` +{ + description : 3 + type : number + value : 3 +} +Runtime.evaluate: `Klass.#field` +{ + description : 3 + type : number + value : 3 +} +Runtime.evaluate: `Klass.#field -= 3` +{ + description : 0 + type : number + value : 0 +} +Runtime.evaluate: `Klass.#field` +{ + description : 0 + type : number + value : 0 +} +Checking private getter-only accessors +Runtime.evaluate: `Klass.#getterOnly` +{ + type : string + value : getterOnly +} +Runtime.evaluate: `Klass.#getterOnly = 1` +{ + className : Error + description : Error: '#getterOnly' was defined without a setter at :1:19 + objectId : + subtype : error + type : object +} +Runtime.evaluate: `Klass.#getterOnly++` +{ + className : Error + description : Error: '#getterOnly' was defined without a setter at :1:18 + objectId : + subtype : error + type : object +} +Runtime.evaluate: `Klass.#getterOnly -= 3` +{ + className : Error + description : Error: '#getterOnly' was defined without a setter at :1:19 + objectId : + subtype : error + type : object +} +Runtime.evaluate: `Klass.#getterOnly` +{ + type : string + value : getterOnly +} +Checking private setter-only accessors +Runtime.evaluate: `Klass.#setterOnly` +{ + className : Error + description : Error: '#setterOnly' was defined without a getter at :1:1 + objectId : + subtype : error + type : object +} +Runtime.evaluate: `Klass.#setterOnly = 1` +{ + description : 1 + type : number + value : 1 +} +Runtime.evaluate: `Klass.#setterOnly++` +{ + className : Error + description : Error: '#setterOnly' was defined without a getter at :1:1 + objectId : + subtype : error + type : object +} +Runtime.evaluate: `Klass.#setterOnly -= 3` +{ + className : Error + description : Error: '#setterOnly' was defined without a getter at :1:1 + objectId : + subtype : error + type : object +} +Runtime.evaluate: `Klass.#field` +{ + type : string + value : setterOnlyCalled +} +Checking private accessors +Runtime.evaluate: `Klass.#accessor` +{ + type : string + value : setterOnlyCalled +} +Runtime.evaluate: `Klass.#accessor = 1` +{ + description : 1 + type : number + value : 1 +} +Runtime.evaluate: `Klass.#field` +{ + description : 1 + type : number + value : 1 +} +Runtime.evaluate: `Klass.#accessor++` +{ + description : 1 + type : number + value : 1 +} +Runtime.evaluate: `Klass.#field` +{ + description : 2 + type : number + value : 2 +} +Runtime.evaluate: `++Klass.#accessor` +{ + type : undefined +} +Runtime.evaluate: `Klass.#field` +{ + description : 3 + type : number + value : 3 +} +Runtime.evaluate: `Klass.#accessor -= 3` +{ + description : 0 + type : number + value : 0 +} +Runtime.evaluate: `Klass.#field` +{ + description : 0 + type : number + value : 0 +} +Checking private methods +Runtime.evaluate: `Klass.#method` +{ + className : Function + description : #method() { return "method"; } + objectId : + type : function +} +Runtime.evaluate: `Klass.#method = 1` +{ + className : Error + description : Error: Private method '#method' is not writable at :1:15 + objectId : + subtype : error + type : object +} +Runtime.evaluate: `Klass.#method++` +{ + className : Error + description : Error: Private method '#method' is not writable at :1:14 + objectId : + subtype : error + type : object +} +Runtime.evaluate: `++Klass.#method` +{ + className : Error + description : Error: Private method '#method' is not writable at :1:9 + objectId : + subtype : error + type : object +} +Runtime.evaluate: `Klass.#method -= 3` +{ + className : Error + description : Error: Private method '#method' is not writable at :1:15 + objectId : + subtype : error + type : object +} diff --git a/test/inspector/runtime/evaluate-private-class-member-static.js b/test/inspector/runtime/evaluate-private-class-member-static.js new file mode 100644 index 0000000000..1a8b5ed141 --- /dev/null +++ b/test/inspector/runtime/evaluate-private-class-member-static.js @@ -0,0 +1,12 @@ +// Copyright 2022 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +utils.load('test/inspector/private-class-member-inspector-test.js'); + +const options = { + type: 'private-static-member', + testRuntime: true, + message: `Evaluate static private class member out of class scope in Runtime.evaluate()` +}; +PrivateClassMemberInspectorTest.runTest(InspectorTest, options); diff --git a/test/inspector/runtime/evaluate-private-class-member.js b/test/inspector/runtime/evaluate-private-class-member.js new file mode 100644 index 0000000000..dd22e790fb --- /dev/null +++ b/test/inspector/runtime/evaluate-private-class-member.js @@ -0,0 +1,12 @@ +// Copyright 2022 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +utils.load('test/inspector/private-class-member-inspector-test.js'); + +const options = { + type: 'private-instance-member', + testRuntime: true, + message: `Evaluate private class member out of class scope in Runtime.evaluate()` +}; +PrivateClassMemberInspectorTest.runTest(InspectorTest, options); diff --git a/test/inspector/testcfg.py b/test/inspector/testcfg.py index f7325aa23b..9ad99c47c6 100644 --- a/test/inspector/testcfg.py +++ b/test/inspector/testcfg.py @@ -11,6 +11,7 @@ from testrunner.outproc import base as outproc PROTOCOL_TEST_JS = "protocol-test.js" WASM_INSPECTOR_JS = "wasm-inspector-test.js" +PRIVATE_MEMBER_TEST_JS = "private-class-member-inspector-test.js" EXPECTED_SUFFIX = "-expected.txt" RESOURCES_FOLDER = "resources" @@ -18,7 +19,7 @@ RESOURCES_FOLDER = "resources" class TestLoader(testsuite.JSTestLoader): @property def excluded_files(self): - return {PROTOCOL_TEST_JS, WASM_INSPECTOR_JS} + return {PROTOCOL_TEST_JS, WASM_INSPECTOR_JS, PRIVATE_MEMBER_TEST_JS} @property def excluded_dirs(self): diff --git a/test/unittests/interpreter/bytecode_expectations/PrivateAccessorAccess.golden b/test/unittests/interpreter/bytecode_expectations/PrivateAccessorAccess.golden index 4e18513dbe..c83f3d98cb 100644 --- a/test/unittests/interpreter/bytecode_expectations/PrivateAccessorAccess.golden +++ b/test/unittests/interpreter/bytecode_expectations/PrivateAccessorAccess.golden @@ -83,7 +83,7 @@ bytecodes: [ /* 48 E> */ B(DefineKeyedOwnProperty), R(this), R(0), U8(0), /* 53 S> */ B(LdaImmutableCurrentContextSlot), U8(3), /* 58 E> */ B(GetKeyedProperty), R(this), U8(2), - B(Wide), B(LdaSmi), I16(308), + B(Wide), B(LdaSmi), I16(310), B(Star2), B(LdaConstant), U8(0), B(Star3), @@ -115,7 +115,7 @@ bytecodes: [ /* 41 E> */ B(DefineKeyedOwnProperty), R(this), R(0), U8(0), /* 46 S> */ B(LdaImmutableCurrentContextSlot), U8(3), /* 51 E> */ B(GetKeyedProperty), R(this), U8(2), - B(Wide), B(LdaSmi), I16(307), + B(Wide), B(LdaSmi), I16(309), B(Star2), B(LdaConstant), U8(0), B(Star3), @@ -149,7 +149,7 @@ bytecodes: [ B(Star2), B(LdaImmutableCurrentContextSlot), U8(3), /* 58 E> */ B(GetKeyedProperty), R(this), U8(2), - B(Wide), B(LdaSmi), I16(308), + B(Wide), B(LdaSmi), I16(310), B(Star3), B(LdaConstant), U8(0), B(Star4), @@ -181,7 +181,7 @@ bytecodes: [ /* 41 E> */ B(DefineKeyedOwnProperty), R(this), R(0), U8(0), /* 46 S> */ B(LdaImmutableCurrentContextSlot), U8(3), /* 51 E> */ B(GetKeyedProperty), R(this), U8(2), - B(Wide), B(LdaSmi), I16(307), + B(Wide), B(LdaSmi), I16(309), B(Star2), B(LdaConstant), U8(0), B(Star3), diff --git a/test/unittests/interpreter/bytecode_expectations/PrivateMethodAccess.golden b/test/unittests/interpreter/bytecode_expectations/PrivateMethodAccess.golden index 2c602e0adc..86596d0d5d 100644 --- a/test/unittests/interpreter/bytecode_expectations/PrivateMethodAccess.golden +++ b/test/unittests/interpreter/bytecode_expectations/PrivateMethodAccess.golden @@ -58,7 +58,7 @@ bytecodes: [ B(Star2), B(LdaImmutableCurrentContextSlot), U8(3), /* 54 E> */ B(GetKeyedProperty), R(this), U8(2), - B(Wide), B(LdaSmi), I16(306), + B(Wide), B(LdaSmi), I16(308), B(Star3), B(LdaConstant), U8(0), B(Star4), @@ -91,7 +91,7 @@ bytecodes: [ /* 44 E> */ B(DefineKeyedOwnProperty), R(this), R(0), U8(0), /* 49 S> */ B(LdaImmutableCurrentContextSlot), U8(3), /* 54 E> */ B(GetKeyedProperty), R(this), U8(2), - B(Wide), B(LdaSmi), I16(306), + B(Wide), B(LdaSmi), I16(308), B(Star2), B(LdaConstant), U8(0), B(Star3), diff --git a/test/unittests/interpreter/bytecode_expectations/StaticPrivateMethodAccess.golden b/test/unittests/interpreter/bytecode_expectations/StaticPrivateMethodAccess.golden index c61f6f6711..59a3f0c97d 100644 --- a/test/unittests/interpreter/bytecode_expectations/StaticPrivateMethodAccess.golden +++ b/test/unittests/interpreter/bytecode_expectations/StaticPrivateMethodAccess.golden @@ -24,7 +24,7 @@ bytecodes: [ B(TestReferenceEqual), R(this), B(Mov), R(this), R(1), B(JumpIfTrue), U8(16), - B(Wide), B(LdaSmi), I16(300), + B(Wide), B(LdaSmi), I16(302), B(Star2), B(LdaConstant), U8(0), B(Star3), @@ -61,13 +61,13 @@ bytecodes: [ B(TestReferenceEqual), R(this), B(Mov), R(this), R(0), B(JumpIfTrue), U8(16), - B(Wide), B(LdaSmi), I16(300), + B(Wide), B(LdaSmi), I16(302), B(Star2), B(LdaConstant), U8(0), B(Star3), /* 61 E> */ B(CallRuntime), U16(Runtime::kNewTypeError), R(2), U8(2), B(Throw), - B(Wide), B(LdaSmi), I16(306), + B(Wide), B(LdaSmi), I16(308), B(Star2), B(LdaConstant), U8(1), B(Star3), @@ -99,13 +99,13 @@ bytecodes: [ B(TestReferenceEqual), R(this), B(Mov), R(this), R(0), B(JumpIfTrue), U8(16), - B(Wide), B(LdaSmi), I16(300), + B(Wide), B(LdaSmi), I16(302), B(Star1), B(LdaConstant), U8(0), B(Star2), /* 61 E> */ B(CallRuntime), U16(Runtime::kNewTypeError), R(1), U8(2), B(Throw), - B(Wide), B(LdaSmi), I16(306), + B(Wide), B(LdaSmi), I16(308), B(Star1), B(LdaConstant), U8(1), B(Star2), @@ -145,7 +145,7 @@ bytecodes: [ B(TestReferenceEqual), R(this), B(Mov), R(this), R(0), B(JumpIfTrue), U8(16), - B(Wide), B(LdaSmi), I16(300), + B(Wide), B(LdaSmi), I16(302), B(Star2), B(LdaConstant), U8(0), B(Star3), @@ -167,7 +167,7 @@ bytecodes: [ B(TestReferenceEqual), R(this), B(Mov), R(this), R(0), B(JumpIfTrue), U8(16), - B(Wide), B(LdaSmi), I16(300), + B(Wide), B(LdaSmi), I16(302), B(Star3), B(LdaConstant), U8(0), B(Star4), @@ -182,7 +182,7 @@ bytecodes: [ B(TestReferenceEqual), R(this), B(Mov), R(this), R(0), B(JumpIfTrue), U8(16), - B(Wide), B(LdaSmi), I16(300), + B(Wide), B(LdaSmi), I16(302), B(Star2), B(LdaConstant), U8(0), B(Star3), @@ -216,13 +216,13 @@ bytecodes: [ B(TestReferenceEqual), R(this), B(Mov), R(this), R(0), B(JumpIfTrue), U8(16), - B(Wide), B(LdaSmi), I16(300), + B(Wide), B(LdaSmi), I16(302), B(Star1), B(LdaConstant), U8(0), B(Star2), /* 65 E> */ B(CallRuntime), U16(Runtime::kNewTypeError), R(1), U8(2), B(Throw), - B(Wide), B(LdaSmi), I16(308), + B(Wide), B(LdaSmi), I16(310), B(Star1), B(LdaConstant), U8(1), B(Star2), @@ -253,13 +253,13 @@ bytecodes: [ B(TestReferenceEqual), R(this), B(Mov), R(this), R(0), B(JumpIfTrue), U8(16), - B(Wide), B(LdaSmi), I16(300), + B(Wide), B(LdaSmi), I16(302), B(Star1), B(LdaConstant), U8(0), B(Star2), /* 58 E> */ B(CallRuntime), U16(Runtime::kNewTypeError), R(1), U8(2), B(Throw), - B(Wide), B(LdaSmi), I16(307), + B(Wide), B(LdaSmi), I16(309), B(Star1), B(LdaConstant), U8(1), B(Star2), @@ -292,13 +292,13 @@ bytecodes: [ B(TestReferenceEqual), R(this), B(Mov), R(this), R(0), B(JumpIfTrue), U8(16), - B(Wide), B(LdaSmi), I16(300), + B(Wide), B(LdaSmi), I16(302), B(Star2), B(LdaConstant), U8(0), B(Star3), /* 65 E> */ B(CallRuntime), U16(Runtime::kNewTypeError), R(2), U8(2), B(Throw), - B(Wide), B(LdaSmi), I16(308), + B(Wide), B(LdaSmi), I16(310), B(Star2), B(LdaConstant), U8(1), B(Star3), @@ -327,7 +327,7 @@ bytecode array length: 19 bytecodes: [ /* 46 S> */ B(LdaImmutableCurrentContextSlot), U8(3), /* 51 E> */ B(GetKeyedProperty), R(this), U8(0), - B(Wide), B(LdaSmi), I16(307), + B(Wide), B(LdaSmi), I16(309), B(Star1), B(LdaConstant), U8(0), B(Star2),