| // Copyright 2025 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. |
| |
| #include "src/objects/script.h" |
| |
| #include "src/ast/ast.h" |
| #include "src/common/globals.h" |
| #include "src/objects/shared-function-info-inl.h" |
| #include "src/tracing/traced-value.h" |
| #include "src/utils/hex-format.h" |
| #include "src/utils/sha-256.h" |
| |
| namespace v8::internal { |
| |
| const char* ToString(Script::Type type) { |
| switch (type) { |
| case Script::Type::kNative: |
| return "native"; |
| case Script::Type::kExtension: |
| return "extension"; |
| case Script::Type::kNormal: |
| return "normal"; |
| #if V8_ENABLE_WEBASSEMBLY |
| case Script::Type::kWasm: |
| return "wasm"; |
| #endif // V8_ENABLE_WEBASSEMBLY |
| case Script::Type::kInspector: |
| return "inspector"; |
| } |
| UNREACHABLE(); |
| } |
| |
| const char* ToString(Script::CompilationType type) { |
| switch (type) { |
| case Script::CompilationType::kHost: |
| return "host"; |
| case Script::CompilationType::kEval: |
| return "eval"; |
| } |
| UNREACHABLE(); |
| } |
| |
| const char* ToString(Script::CompilationState type) { |
| switch (type) { |
| case Script::CompilationState::kInitial: |
| return "initial"; |
| case Script::CompilationState::kCompiled: |
| return "compiled"; |
| } |
| UNREACHABLE(); |
| } |
| |
| template <typename IsolateT> |
| MaybeHandle<SharedFunctionInfo> Script::FindSharedFunctionInfo( |
| DirectHandle<Script> script, IsolateT* isolate, |
| FunctionLiteral* function_literal) { |
| DCHECK(function_literal->shared_function_info().is_null()); |
| int function_literal_id = function_literal->function_literal_id(); |
| CHECK_GE(function_literal_id, 0); |
| // If this check fails, the problem is most probably the function id |
| // renumbering done by AstFunctionLiteralIdReindexer; in particular, that |
| // AstTraversalVisitor doesn't recurse properly in the construct which |
| // triggers the mismatch. |
| CHECK_LT(static_cast<uint32_t>(function_literal_id), |
| script->infos()->ulength().value()); |
| Tagged<MaybeObject> shared = |
| script->infos()->get(function_literal_id, kAcquireLoad); |
| Tagged<HeapObject> heap_object; |
| if (!shared.GetHeapObject(&heap_object) || IsUndefined(heap_object)) { |
| return MaybeHandle<SharedFunctionInfo>(); |
| } |
| Handle<SharedFunctionInfo> result(Cast<SharedFunctionInfo>(heap_object), |
| isolate); |
| CHECK(Is<SharedFunctionInfo>(*result)); |
| CHECK_EQ(result->StartPosition(), function_literal->start_position()); |
| CHECK_EQ(result->EndPosition(), function_literal->end_position()); |
| CHECK_EQ(result->function_literal_id(kRelaxedLoad), |
| function_literal->function_literal_id()); |
| function_literal->set_shared_function_info(result); |
| return result; |
| } |
| |
| template MaybeHandle<SharedFunctionInfo> Script::FindSharedFunctionInfo( |
| DirectHandle<Script> script, Isolate* isolate, |
| FunctionLiteral* function_literal); |
| template MaybeHandle<SharedFunctionInfo> Script::FindSharedFunctionInfo( |
| DirectHandle<Script> script, LocalIsolate* isolate, |
| FunctionLiteral* function_literal); |
| |
| Script::Iterator::Iterator(Isolate* isolate) |
| : iterator_(isolate->heap()->script_list()) {} |
| |
| Tagged<Script> Script::Iterator::Next() { |
| Tagged<Object> o = iterator_.Next(); |
| if (o != Tagged<Object>()) { |
| return Cast<Script>(o); |
| } |
| return Tagged<Script>(); |
| } |
| |
| // static |
| int Script::GetEvalPosition(Isolate* isolate, DirectHandle<Script> script) { |
| DCHECK(script->compilation_type() == Script::CompilationType::kEval); |
| int position = script->eval_from_position(); |
| if (position < 0) { |
| // Due to laziness, the position may not have been translated from code |
| // offset yet, which would be encoded as negative integer. In that case, |
| // translate and set the position. |
| if (!script->has_eval_from_shared()) { |
| position = 0; |
| } else { |
| DirectHandle<SharedFunctionInfo> shared(script->eval_from_shared(), |
| isolate); |
| SharedFunctionInfo::EnsureSourcePositionsAvailable(isolate, shared); |
| position = |
| shared->abstract_code(isolate)->SourcePosition(isolate, -position); |
| } |
| DCHECK_GE(position, 0); |
| script->set_eval_from_position(position); |
| } |
| return position; |
| } |
| |
| void Script::set_eval_from_shared(Tagged<SharedFunctionInfo> shared, |
| WriteBarrierMode mode) { |
| DCHECK(!is_wrapped()); |
| set_eval_from_shared_or_wrapped_arguments(shared, mode); |
| } |
| |
| Tagged<SharedFunctionInfo> Script::eval_from_shared() const { |
| DCHECK(has_eval_from_shared()); |
| return Cast<SharedFunctionInfo>(eval_from_shared_or_wrapped_arguments()); |
| } |
| |
| Tagged<Script> Script::GetEvalOrigin() { |
| DisallowGarbageCollection no_gc; |
| Tagged<Script> origin_script = this; |
| while (origin_script->has_eval_from_shared()) { |
| Tagged<HeapObject> maybe_script = |
| origin_script->eval_from_shared()->script(); |
| CHECK(IsScript(maybe_script)); |
| origin_script = Cast<Script>(maybe_script); |
| } |
| return origin_script; |
| } |
| |
| String::LineEndsVector Script::GetLineEnds(Isolate* isolate, |
| DirectHandle<Script> script) { |
| DCHECK(!script->has_line_ends()); |
| Tagged<Object> src_obj = script->source(); |
| if (IsString(src_obj)) { |
| DirectHandle<String> src(Cast<String>(src_obj), isolate); |
| return String::CalculateLineEndsVector(isolate, src, true); |
| } |
| |
| return String::LineEndsVector(); |
| } |
| |
| template <typename IsolateT> |
| // static |
| void Script::InitLineEndsInternal(IsolateT* isolate, |
| DirectHandle<Script> script) { |
| DCHECK(!script->has_line_ends()); |
| DCHECK(script->CanHaveLineEnds()); |
| Tagged<Object> src_obj = script->source(); |
| if (!IsString(src_obj)) { |
| DCHECK(IsUndefined(src_obj)); |
| script->set_line_ends(ReadOnlyRoots(isolate).empty_fixed_array()); |
| } else { |
| DCHECK(IsString(src_obj)); |
| DirectHandle<String> src(Cast<String>(src_obj), isolate); |
| DirectHandle<FixedArray> array = |
| String::CalculateLineEnds(isolate, src, true); |
| script->set_line_ends(*array); |
| } |
| DCHECK(IsFixedArray(script->line_ends())); |
| DCHECK(script->has_line_ends()); |
| } |
| |
| void Script::SetSource(Isolate* isolate, DirectHandle<Script> script, |
| DirectHandle<String> source) { |
| script->set_source(*source); |
| if (isolate->NeedsSourcePositions()) { |
| InitLineEnds(isolate, script); |
| } else if (script->line_ends() == |
| ReadOnlyRoots(isolate).empty_fixed_array()) { |
| DCHECK(script->has_line_ends()); |
| script->set_line_ends(Smi::zero()); |
| DCHECK(!script->has_line_ends()); |
| } |
| } |
| |
| template V8_EXPORT_PRIVATE void Script::InitLineEndsInternal( |
| Isolate* isolate, DirectHandle<Script> script); |
| template V8_EXPORT_PRIVATE void Script::InitLineEndsInternal( |
| LocalIsolate* isolate, DirectHandle<Script> script); |
| |
| bool Script::GetPositionInfo(DirectHandle<Script> script, int position, |
| PositionInfo* info, OffsetFlag offset_flag) { |
| #if V8_ENABLE_WEBASSEMBLY |
| // For wasm, we do not create an artificial line_ends array, but do the |
| // translation directly. |
| #ifdef DEBUG |
| if (script->type() == Type::kWasm) { |
| DCHECK(script->has_line_ends()); |
| DCHECK_EQ(Cast<FixedArray>(script->line_ends())->ulength().value(), 0); |
| } |
| #endif // DEBUG |
| #endif // V8_ENABLE_WEBASSEMBLY |
| InitLineEnds(Isolate::Current(), script); |
| return script->GetPositionInfo(position, info, offset_flag); |
| } |
| |
| bool Script::IsSubjectToDebugging() const { |
| switch (type()) { |
| case Type::kNormal: |
| #if V8_ENABLE_WEBASSEMBLY |
| case Type::kWasm: |
| #endif // V8_ENABLE_WEBASSEMBLY |
| return true; |
| case Type::kNative: |
| case Type::kInspector: |
| case Type::kExtension: |
| return false; |
| } |
| UNREACHABLE(); |
| } |
| |
| bool Script::IsUserJavaScript() const { |
| return type() == Script::Type::kNormal; |
| } |
| |
| void Script::TraceScriptRundown() { |
| DisallowGarbageCollection no_gc; |
| Isolate* isolate = Isolate::Current(); |
| Tagged<Object> context_value = isolate->native_context()->debug_context_id(); |
| int contextId = (IsSmi(context_value)) ? Smi::ToInt(context_value) : 0; |
| auto value = v8::tracing::TracedValue::Create(); |
| value->SetInteger("scriptId", this->id()); |
| value->SetInteger("executionContextId", contextId); |
| value->SetString("isolate", std::to_string(isolate->debug()->IsolateId())); |
| value->SetBoolean("isModule", this->origin_options().IsModule()); |
| value->SetBoolean("hasSourceUrl", this->HasSourceURLComment()); |
| if (this->HasValidSource()) { |
| if (this->HasSourceURLComment()) { |
| value->SetString("sourceUrl", |
| Cast<String>(this->source_url())->ToCString().get()); |
| } |
| if (this->HasSourceMappingURLComment()) { |
| Tagged<String> sourceMapUrl = Cast<String>(this->source_mapping_url()); |
| // Source maps can be huge. Don't ever put a huge data url source map in |
| // the trace. Also omit unusually large regular URLs. |
| constexpr int kMaxSourceMapUrlLength = 2048; |
| if (sourceMapUrl->length() > kMaxSourceMapUrlLength) { |
| // Signal that there is a source map - clients may wish to request it by |
| // other means (such as via CDP) or otherwise warn that a source map was |
| // present but elided. |
| value->SetBoolean("sourceMapUrlElided", true); |
| } else { |
| value->SetString("sourceMapUrl", sourceMapUrl->ToCString().get()); |
| } |
| } |
| } |
| if (IsString(this->name())) { |
| value->SetString("url", Cast<String>(this->name())->ToCString().get()); |
| } |
| TRACE_EVENT(TRACE_DISABLED_BY_DEFAULT("devtools.v8-source-rundown"), |
| "ScriptCatchup", "data", std::move(value)); |
| } |
| |
| void Script::TraceScriptRundownSources() { |
| DisallowGarbageCollection no_gc; |
| Isolate* isolate = Isolate::Current(); |
| if (!IsString(this->source())) return; |
| Tagged<String> source = Cast<String>(this->source()); |
| auto script_id = this->id(); |
| int32_t source_length = source->length(); |
| |
| const int32_t kSourceMaxLength = 25000000; // 25mb |
| const int32_t kSplitMaxLength = 1000000; // 1mb |
| if (source_length > kSourceMaxLength) { |
| auto value = v8::tracing::TracedValue::Create(); |
| value->SetString("isolate", std::to_string(isolate->debug()->IsolateId())); |
| value->SetInteger("scriptId", script_id); |
| value->SetInteger("length", source_length); |
| value->SetInteger("limit", kSourceMaxLength); |
| TRACE_EVENT(TRACE_DISABLED_BY_DEFAULT("devtools.v8-source-rundown-sources"), |
| "TooLargeScriptCatchup", "data", std::move(value)); |
| } else if (source_length <= kSplitMaxLength) { |
| auto value = v8::tracing::TracedValue::Create(); |
| value->SetString("isolate", std::to_string(isolate->debug()->IsolateId())); |
| value->SetInteger("scriptId", script_id); |
| value->SetInteger("length", source_length); |
| value->SetString("sourceText", source->ToCString().get()); |
| TRACE_EVENT(TRACE_DISABLED_BY_DEFAULT("devtools.v8-source-rundown-sources"), |
| "ScriptCatchup", "data", std::move(value)); |
| } else { |
| int32_t split_count = source_length / kSplitMaxLength + 1; |
| for (int32_t i = 0; i < split_count; i++) { |
| int32_t begin = i * kSplitMaxLength; |
| int32_t length = std::min(kSplitMaxLength, source_length - begin); |
| auto split_trace_value = v8::tracing::TracedValue::Create(); |
| split_trace_value->SetInteger("splitIndex", i); |
| split_trace_value->SetInteger("splitCount", split_count); |
| split_trace_value->SetString( |
| "isolate", std::to_string(isolate->debug()->IsolateId())); |
| split_trace_value->SetInteger("scriptId", script_id); |
| split_trace_value->SetString("sourceText", |
| source->ToCString(begin, length).get()); |
| TRACE_EVENT( |
| TRACE_DISABLED_BY_DEFAULT("devtools.v8-source-rundown-sources"), |
| "LargeScriptCatchup", "data", std::move(split_trace_value)); |
| } |
| } |
| } |
| |
| void Script::AddPositionInfoOffset(PositionInfo* info, |
| OffsetFlag offset_flag) const { |
| // Add offsets if requested. |
| if (offset_flag == OffsetFlag::kWithOffset) { |
| if (info->line == 0) { |
| info->column += column_offset(); |
| } |
| info->line += line_offset(); |
| } else { |
| DCHECK_EQ(offset_flag, OffsetFlag::kNoOffset); |
| } |
| } |
| |
| namespace { |
| |
| template <typename Char> |
| bool GetPositionInfoSlowImpl(base::Vector<Char> source, int position, |
| Script::PositionInfo* info) { |
| DCHECK(DisallowPositionInfoSlow::IsAllowed()); |
| if (position < 0) { |
| position = 0; |
| } |
| int line = 0; |
| const auto begin = std::cbegin(source); |
| const auto end = std::cend(source); |
| for (auto line_begin = begin; line_begin < end;) { |
| const auto line_end = std::find(line_begin, end, '\n'); |
| if (position <= (line_end - begin)) { |
| info->line = line; |
| info->column = static_cast<int>((begin + position) - line_begin); |
| info->line_start = static_cast<int>(line_begin - begin); |
| info->line_end = static_cast<int>(line_end - begin); |
| return true; |
| } |
| ++line; |
| line_begin = line_end + 1; |
| } |
| return false; |
| } |
| bool GetPositionInfoSlow(const Tagged<Script> script, int position, |
| const DisallowGarbageCollection& no_gc, |
| Script::PositionInfo* info) { |
| if (!IsString(script->source())) { |
| return false; |
| } |
| auto source = Cast<String>(script->source()); |
| const auto flat = source->GetFlatContent(no_gc); |
| return flat.IsOneByte() |
| ? GetPositionInfoSlowImpl(flat.ToOneByteVector(), position, info) |
| : GetPositionInfoSlowImpl(flat.ToUC16Vector(), position, info); |
| } |
| |
| int GetLineEnd(const String::LineEndsVector& vector, int line) { |
| return vector[line]; |
| } |
| |
| int GetLineEnd(const Tagged<FixedArray>& array, int line) { |
| return Smi::ToInt(array->get(line)); |
| } |
| |
| uint32_t GetLength(const String::LineEndsVector& vector) { |
| return static_cast<uint32_t>(vector.size()); |
| } |
| |
| uint32_t GetLength(const Tagged<FixedArray>& array) { |
| return array->ulength().value(); |
| } |
| |
| template <typename LineEndsContainer> |
| bool GetLineEndsContainerPositionInfo(const LineEndsContainer& ends, |
| int position, Script::PositionInfo* info, |
| const DisallowGarbageCollection& no_gc) { |
| const uint32_t ends_len = GetLength(ends); |
| if (ends_len == 0) return false; |
| |
| // Return early on invalid positions. Negative positions behave as if 0 was |
| // passed, and positions beyond the end of the script return as failure. |
| if (position < 0) { |
| position = 0; |
| } else if (position > GetLineEnd(ends, static_cast<int>(ends_len - 1))) { |
| return false; |
| } |
| |
| // Determine line number by doing a binary search on the line ends array. |
| if (GetLineEnd(ends, 0) >= position) { |
| info->line = 0; |
| info->line_start = 0; |
| info->column = position; |
| } else { |
| int left = 0; |
| int right = static_cast<int>(ends_len - 1); |
| |
| while (right > 0) { |
| DCHECK_LE(left, right); |
| const int mid = left + (right - left) / 2; |
| if (position > GetLineEnd(ends, mid)) { |
| left = mid + 1; |
| } else if (position <= GetLineEnd(ends, mid - 1)) { |
| right = mid - 1; |
| } else { |
| info->line = mid; |
| break; |
| } |
| } |
| DCHECK(GetLineEnd(ends, info->line) >= position && |
| GetLineEnd(ends, info->line - 1) < position); |
| info->line_start = GetLineEnd(ends, info->line - 1) + 1; |
| info->column = position - info->line_start; |
| } |
| |
| return true; |
| } |
| |
| } // namespace |
| |
| template <typename LineEndsContainer> |
| bool Script::GetPositionInfoInternal( |
| const LineEndsContainer& ends, int position, Script::PositionInfo* info, |
| const DisallowGarbageCollection& no_gc) const { |
| if (!GetLineEndsContainerPositionInfo(ends, position, info, no_gc)) { |
| return false; |
| } |
| |
| // Line end is position of the linebreak character. |
| info->line_end = GetLineEnd(ends, info->line); |
| if (info->line_end > 0) { |
| DCHECK(IsString(source())); |
| Tagged<String> src = Cast<String>(source()); |
| if (src->length() >= static_cast<uint32_t>(info->line_end) && |
| src->Get(info->line_end - 1) == '\r') { |
| info->line_end--; |
| } |
| } |
| |
| return true; |
| } |
| |
| template bool Script::GetPositionInfoInternal<String::LineEndsVector>( |
| const String::LineEndsVector& ends, int position, |
| Script::PositionInfo* info, const DisallowGarbageCollection& no_gc) const; |
| template bool Script::GetPositionInfoInternal<Tagged<FixedArray>>( |
| const Tagged<FixedArray>& ends, int position, Script::PositionInfo* info, |
| const DisallowGarbageCollection& no_gc) const; |
| |
| bool Script::GetPositionInfo(int position, PositionInfo* info, |
| OffsetFlag offset_flag) const { |
| DisallowGarbageCollection no_gc; |
| |
| #if V8_ENABLE_WEBASSEMBLY |
| // For wasm, we use the byte offset as the column. |
| if (type() == Script::Type::kWasm) { |
| DCHECK_LE(0, position); |
| CppGCManaged<wasm::NativeModule>::Ptr native_module = |
| wasm_native_module(); |
| const wasm::WasmModule* module = native_module->module(); |
| if (module->functions.empty()) return false; |
| info->line = 0; |
| info->column = position; |
| info->line_start = module->functions[0].code.offset(); |
| info->line_end = module->functions.back().code.end_offset(); |
| return true; |
| } |
| #endif // V8_ENABLE_WEBASSEMBLY |
| |
| if (!has_line_ends()) { |
| // Slow mode: we do not have line_ends. We have to iterate through source. |
| if (!GetPositionInfoSlow(this, position, no_gc, info)) { |
| return false; |
| } |
| } else { |
| DCHECK(has_line_ends()); |
| Tagged<FixedArray> ends = Cast<FixedArray>(line_ends()); |
| |
| if (!GetPositionInfoInternal(ends, position, info, no_gc)) return false; |
| } |
| |
| AddPositionInfoOffset(info, offset_flag); |
| |
| return true; |
| } |
| |
| bool Script::GetPositionInfoWithLineEnds( |
| int position, PositionInfo* info, const String::LineEndsVector& line_ends, |
| OffsetFlag offset_flag) const { |
| DisallowGarbageCollection no_gc; |
| if (!GetPositionInfoInternal(line_ends, position, info, no_gc)) return false; |
| |
| AddPositionInfoOffset(info, offset_flag); |
| |
| return true; |
| } |
| |
| bool Script::GetLineColumnWithLineEnds( |
| int position, int& line, int& column, |
| const String::LineEndsVector& line_ends) { |
| DisallowGarbageCollection no_gc; |
| PositionInfo info; |
| if (!GetLineEndsContainerPositionInfo(line_ends, position, &info, no_gc)) { |
| line = -1; |
| column = -1; |
| return false; |
| } |
| |
| line = info.line; |
| column = info.column; |
| |
| return true; |
| } |
| |
| int Script::GetColumnNumber(DirectHandle<Script> script, int code_pos) { |
| PositionInfo info; |
| GetPositionInfo(script, code_pos, &info); |
| return info.column; |
| } |
| |
| int Script::GetColumnNumber(int code_pos) const { |
| PositionInfo info; |
| GetPositionInfo(code_pos, &info); |
| return info.column; |
| } |
| |
| int Script::GetLineNumber(DirectHandle<Script> script, int code_pos) { |
| PositionInfo info; |
| GetPositionInfo(script, code_pos, &info); |
| return info.line; |
| } |
| |
| int Script::GetLineNumber(int code_pos) const { |
| PositionInfo info; |
| GetPositionInfo(code_pos, &info); |
| return info.line; |
| } |
| |
| Tagged<Object> Script::GetNameOrSourceURL() { |
| // Keep in sync with ScriptNameOrSourceURL in messages.js. |
| if (!IsUndefined(source_url())) return source_url(); |
| return name(); |
| } |
| |
| // static |
| DirectHandle<String> Script::GetScriptHash(Isolate* isolate, |
| DirectHandle<Script> script, |
| bool forceForInspector) { |
| if (script->origin_options().IsOpaque() && !forceForInspector) { |
| return isolate->factory()->empty_string(); |
| } |
| |
| { |
| Tagged<Object> maybe_source_hash = script->source_hash(); |
| if (IsString(maybe_source_hash)) { |
| DirectHandle<String> precomputed(Cast<String>(maybe_source_hash), |
| isolate); |
| if (precomputed->length() > 0) { |
| return precomputed; |
| } |
| } |
| } |
| |
| DirectHandle<String> src_text; |
| { |
| Tagged<Object> maybe_script_source = script->source(); |
| |
| if (!IsString(maybe_script_source)) { |
| return isolate->factory()->empty_string(); |
| } |
| src_text = direct_handle(Cast<String>(maybe_script_source), isolate); |
| } |
| |
| char formatted_hash[kSizeOfFormattedSha256Digest]; |
| |
| std::unique_ptr<char[]> string_val = src_text->ToCString(); |
| size_t len = strlen(string_val.get()); |
| uint8_t hash[kSizeOfSha256Digest]; |
| SHA256_hash(string_val.get(), len, hash); |
| FormatBytesToHex(formatted_hash, kSizeOfFormattedSha256Digest, hash, |
| kSizeOfSha256Digest); |
| formatted_hash[kSizeOfSha256Digest * 2] = '\0'; |
| |
| DirectHandle<String> result = |
| isolate->factory()->NewStringFromAsciiChecked(formatted_hash); |
| script->set_source_hash(*result); |
| return result; |
| } |
| |
| } // namespace v8::internal |