// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ui' show hashValues; import 'package:meta/meta.dart'; import 'constants.dart'; import 'object.dart'; /// A object representation of a frame from a stack trace. /// /// {@tool snippet} /// /// This example creates a traversable list of parsed [StackFrame] objects from /// the current [StackTrace]. /// /// ```dart /// final List<StackFrame> currentFrames = StackFrame.fromStackTrace(StackTrace.current); /// ``` /// {@end-tool} @immutable class StackFrame { /// Creates a new StackFrame instance. /// /// All parameters must not be null. The [className] may be the empty string /// if there is no class (e.g. for a top level library method). const StackFrame({ required this.number, required this.column, required this.line, required this.packageScheme, required this.package, required this.packagePath, this.className = '', required this.method, this.isConstructor = false, required this.source, }) : assert(number != null), assert(column != null), assert(line != null), assert(method != null), assert(packageScheme != null), assert(package != null), assert(packagePath != null), assert(className != null), assert(isConstructor != null), assert(source != null); /// A stack frame representing an asynchronous suspension. static const StackFrame asynchronousSuspension = StackFrame( number: -1, column: -1, line: -1, method: 'asynchronous suspension', packageScheme: '', package: '', packagePath: '', source: '<asynchronous suspension>', ); /// A stack frame representing a Dart elided stack overflow frame. static const StackFrame stackOverFlowElision = StackFrame( number: -1, column: -1, line: -1, method: '...', packageScheme: '', package: '', packagePath: '', source: '...', ); /// Parses a list of [StackFrame]s from a [StackTrace] object. /// /// This is normally useful with [StackTrace.current]. static List<StackFrame> fromStackTrace(StackTrace stack) { assert(stack != null); return fromStackString(stack.toString()); } /// Parses a list of [StackFrame]s from the [StackTrace.toString] method. static List<StackFrame> fromStackString(String stack) { assert(stack != null); return stack .trim() .split('\n') .where((String line) => line.isNotEmpty) .map(fromStackTraceLine) // On the Web in non-debug builds the stack trace includes the exception // message that precedes the stack trace itself. fromStackTraceLine will // return null in that case. We will skip it here. .whereType<StackFrame>() .toList(); } static StackFrame? _parseWebFrame(String line) { if (kDebugMode) { return _parseWebDebugFrame(line); } else { return _parseWebNonDebugFrame(line); } } static StackFrame _parseWebDebugFrame(String line) { // This RegExp is only partially correct for flutter run/test differences. // https://github.com/flutter/flutter/issues/52685 final bool hasPackage = line.startsWith('package'); final RegExp parser = hasPackage ? RegExp(r'^(package.+) (\d+):(\d+)\s+(.+)$') : RegExp(r'^(.+) (\d+):(\d+)\s+(.+)$'); Match? match = parser.firstMatch(line); assert(match != null, 'Expected $line to match $parser.'); match = match!; String package = '<unknown>'; String packageScheme = '<unknown>'; String packagePath = '<unknown>'; if (hasPackage) { packageScheme = 'package'; final Uri packageUri = Uri.parse(match.group(1)!); package = packageUri.pathSegments[0]; packagePath = packageUri.path.replaceFirst(packageUri.pathSegments[0] + '/', ''); } return StackFrame( number: -1, packageScheme: packageScheme, package: package, packagePath: packagePath, line: int.parse(match.group(2)!), column: int.parse(match.group(3)!), className: '<unknown>', method: match.group(4)!, source: line, ); } // Non-debug builds do not point to dart code but compiled JavaScript, so // line numbers are meaningless. We only attempt to parse the class and // method name, which is more or less readable in profile builds, and // minified in release builds. static final RegExp _webNonDebugFramePattern = RegExp(r'^\s*at ([^\s]+).*$'); // Parses `line` as a stack frame in profile and release Web builds. If not // recognized as a stack frame, returns null. static StackFrame? _parseWebNonDebugFrame(String line) { final Match? match = _webNonDebugFramePattern.firstMatch(line); if (match == null) { // On the Web in non-debug builds the stack trace includes the exception // message that precedes the stack trace itself. Example: // // TypeError: Cannot read property 'hello$0' of null // at _GalleryAppState.build$1 (http://localhost:8080/main.dart.js:149790:13) // at StatefulElement.build$0 (http://localhost:8080/main.dart.js:129138:37) // at StatefulElement.performRebuild$0 (http://localhost:8080/main.dart.js:129032:23) // // Instead of crashing when a line is not recognized as a stack frame, we // return null. The caller, such as fromStackString, can then just skip // this frame. return null; } final List<String> classAndMethod = match.group(1)!.split('.'); final String className = classAndMethod.length > 1 ? classAndMethod.first : '<unknown>'; final String method = classAndMethod.length > 1 ? classAndMethod.skip(1).join('.') : classAndMethod.single; return StackFrame( number: -1, packageScheme: '<unknown>', package: '<unknown>', packagePath: '<unknown>', line: -1, column: -1, className: className, method: method, source: line, ); } /// Parses a single [StackFrame] from a single line of a [StackTrace]. static StackFrame? fromStackTraceLine(String line) { assert(line != null); if (line == '<asynchronous suspension>') { return asynchronousSuspension; } else if (line == '...') { return stackOverFlowElision; } assert( line != '===== asynchronous gap ===========================', 'Got a stack frame from package:stack_trace, where a vm or web frame was expected. ' 'This can happen if FlutterError.demangleStackTrace was not set in an environment ' 'that propagates non-standard stack traces to the framework, such as during tests.' ); // Web frames. if (!line.startsWith('#')) { return _parseWebFrame(line); } final RegExp parser = RegExp(r'^#(\d+) +(.+) \((.+?):?(\d+){0,1}:?(\d+){0,1}\)$'); Match? match = parser.firstMatch(line); assert(match != null, 'Expected $line to match $parser.'); match = match!; bool isConstructor = false; String className = ''; String method = match.group(2)!.replaceAll('.<anonymous closure>', ''); if (method.startsWith('new')) { className = method.split(' ')[1]; method = ''; if (className.contains('.')) { final List<String> parts = className.split('.'); className = parts[0]; method = parts[1]; } isConstructor = true; } else if (method.contains('.')) { final List<String> parts = method.split('.'); className = parts[0]; method = parts[1]; } final Uri packageUri = Uri.parse(match.group(3)!); String package = '<unknown>'; String packagePath = packageUri.path; if (packageUri.scheme == 'dart' || packageUri.scheme == 'package') { package = packageUri.pathSegments[0]; packagePath = packageUri.path.replaceFirst(packageUri.pathSegments[0] + '/', ''); } return StackFrame( number: int.parse(match.group(1)!), className: className, method: method, packageScheme: packageUri.scheme, package: package, packagePath: packagePath, line: match.group(4) == null ? -1 : int.parse(match.group(4)!), column: match.group(5) == null ? -1 : int.parse(match.group(5)!), isConstructor: isConstructor, source: line, ); } /// The original source of this stack frame. final String source; /// The zero-indexed frame number. /// /// This value may be -1 to indicate an unknown frame number. final int number; /// The scheme of the package for this frame, e.g. "dart" for /// dart:core/errors_patch.dart or "package" for /// package:flutter/src/widgets/text.dart. /// /// The path property refers to the source file. final String packageScheme; /// The package for this frame, e.g. "core" for /// dart:core/errors_patch.dart or "flutter" for /// package:flutter/src/widgets/text.dart. final String package; /// The path of the file for this frame, e.g. "errors_patch.dart" for /// dart:core/errors_patch.dart or "src/widgets/text.dart" for /// package:flutter/src/widgets/text.dart. final String packagePath; /// The source line number. final int line; /// The source column number. final int column; /// The class name, if any, for this frame. /// /// This may be null for top level methods in a library or anonymous closure /// methods. final String className; /// The method name for this frame. /// /// This will be an empty string if the stack frame is from the default /// constructor. final String method; /// Whether or not this was thrown from a constructor. final bool isConstructor; @override int get hashCode => hashValues(number, package, line, column, className, method, source); @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; return other is StackFrame && other.number == number && other.package == package && other.line == line && other.column == column && other.className == className && other.method == method && other.source == source; } @override String toString() => '${objectRuntimeType(this, 'StackFrame')}(#$number, $packageScheme:$package/$packagePath:$line:$column, className: $className, method: $method)'; }