mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
0825c2b2e7
Summary: This diff adds handling for syntax errors. ## Strategy To do this we introduce a new log level type syntax, giving us these levels with semantics: - `warn` - console warns, show collapsed, dismissible - `error` - console errors, show collapsed, dismissible - `fatal` - thrown exceptions, show expanded, not dismissible - `syntax` - thrown exceptions for invalid syntax, show expanded, not dismissible Syntax errors shows expanded, covers all other errors, and are only dismissible when the syntax error is fixed and updated with Fast Refresh. Once the syntax error is fixed, it reveals any previously covered fatals, errors, or warnings behind it In many ways, this makes syntax errors the highest level error. ## Visuals Syntax errors also have their own display formatting. Stack traces for syntax errors don't make sense, so we don't show them. Instead, we show the syntax error message and a code frame for the error. The code frame is also updated so that is doesn't wrap and is horizontally scrollable, making it easier to read. ## Detecting syntax errors To detect syntax errors we've updated `LogBoxData.addException` to call the parse function `parseLogBoxException`. This method will perform a regex on the error message to detect: - file name - location - error message - codeframe If this regex fails for any reason to find all four parts, we'll fall back to a fatal. Over time we'll update this regex to be more robust and handle more cases we've missed. Changelog: [Internal] Reviewed By: motiz88 Differential Revision: D18278862 fbshipit-source-id: 59069aba38a27c44787e5248b2973c3a345c4a0a
210 lines
5.4 KiB
JavaScript
210 lines
5.4 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
* @format
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
import UTFSequence from '../../UTFSequence';
|
|
import stringifySafe from '../../Utilities/stringifySafe';
|
|
import type {LogLevel} from './LogBoxLog';
|
|
import type {ExceptionData} from '../../Core/NativeExceptionsManager';
|
|
import type {Stack} from './LogBoxSymbolication';
|
|
|
|
export type Category = string;
|
|
export type CodeFrame = $ReadOnly<{|
|
|
content: string,
|
|
location: string,
|
|
fileName: string,
|
|
|}>;
|
|
export type Message = $ReadOnly<{|
|
|
content: string,
|
|
substitutions: $ReadOnlyArray<
|
|
$ReadOnly<{|
|
|
length: number,
|
|
offset: number,
|
|
|}>,
|
|
>,
|
|
|}>;
|
|
|
|
export type ComponentStack = $ReadOnlyArray<
|
|
$ReadOnly<{|
|
|
component: string,
|
|
location: string,
|
|
|}>,
|
|
>;
|
|
|
|
const SUBSTITUTION = UTFSequence.BOM + '%s';
|
|
|
|
export function parseCategory(
|
|
args: $ReadOnlyArray<mixed>,
|
|
): $ReadOnly<{|
|
|
category: Category,
|
|
message: Message,
|
|
|}> {
|
|
const categoryParts = [];
|
|
const contentParts = [];
|
|
const substitutionOffsets = [];
|
|
|
|
const remaining = [...args];
|
|
if (typeof remaining[0] === 'string') {
|
|
const formatString = String(remaining.shift());
|
|
const formatStringParts = formatString.split('%s');
|
|
const substitutionCount = formatStringParts.length - 1;
|
|
const substitutions = remaining.splice(0, substitutionCount);
|
|
|
|
let categoryString = '';
|
|
let contentString = '';
|
|
|
|
let substitutionIndex = 0;
|
|
for (const formatStringPart of formatStringParts) {
|
|
categoryString += formatStringPart;
|
|
contentString += formatStringPart;
|
|
|
|
if (substitutionIndex < substitutionCount) {
|
|
if (substitutionIndex < substitutions.length) {
|
|
// Don't stringify a string type.
|
|
// It adds quotation mark wrappers around the string,
|
|
// which causes the LogBox to look odd.
|
|
const substitution =
|
|
typeof substitutions[substitutionIndex] === 'string'
|
|
? substitutions[substitutionIndex]
|
|
: stringifySafe(substitutions[substitutionIndex]);
|
|
substitutionOffsets.push({
|
|
length: substitution.length,
|
|
offset: contentString.length,
|
|
});
|
|
|
|
categoryString += SUBSTITUTION;
|
|
contentString += substitution;
|
|
} else {
|
|
substitutionOffsets.push({
|
|
length: 2,
|
|
offset: contentString.length,
|
|
});
|
|
|
|
categoryString += '%s';
|
|
contentString += '%s';
|
|
}
|
|
|
|
substitutionIndex++;
|
|
}
|
|
}
|
|
|
|
categoryParts.push(categoryString);
|
|
contentParts.push(contentString);
|
|
}
|
|
|
|
const remainingArgs = remaining.map(arg => {
|
|
// Don't stringify a string type.
|
|
// It adds quotation mark wrappers around the string,
|
|
// which causes the LogBox to look odd.
|
|
return typeof arg === 'string' ? arg : stringifySafe(arg);
|
|
});
|
|
categoryParts.push(...remainingArgs);
|
|
contentParts.push(...remainingArgs);
|
|
|
|
return {
|
|
category: categoryParts.join(' '),
|
|
message: {
|
|
content: contentParts.join(' '),
|
|
substitutions: substitutionOffsets,
|
|
},
|
|
};
|
|
}
|
|
export function parseComponentStack(message: string): ComponentStack {
|
|
return message
|
|
.split(/\n {4}in /g)
|
|
.map(s => {
|
|
if (!s) {
|
|
return null;
|
|
}
|
|
let [component, location] = s.split(/ \(at /);
|
|
if (!location) {
|
|
[component, location] = s.split(/ \(/);
|
|
}
|
|
return {component, location: location && location.replace(')', '')};
|
|
})
|
|
.filter(Boolean);
|
|
}
|
|
|
|
export function parseLogBoxException(
|
|
error: ExceptionData,
|
|
): {|
|
|
level: LogLevel,
|
|
category: Category,
|
|
message: Message,
|
|
codeFrame?: CodeFrame,
|
|
stack: Stack,
|
|
componentStack?: ComponentStack,
|
|
|} {
|
|
const message =
|
|
error.originalMessage != null ? error.originalMessage : 'Unknown';
|
|
const match = message.match(
|
|
/(?:TransformError )?(?:SyntaxError: )(.*): (.*) (.*)\n\n([\s\S]+)/,
|
|
);
|
|
|
|
if (!match) {
|
|
return {
|
|
level: error.isFatal ? 'fatal' : 'error',
|
|
stack: error.stack,
|
|
componentStack:
|
|
error.componentStack != null
|
|
? parseComponentStack(error.componentStack)
|
|
: [],
|
|
...parseCategory([message]),
|
|
};
|
|
}
|
|
|
|
const [fileName, content, location, codeFrame] = match.slice(1);
|
|
return {
|
|
level: 'syntax',
|
|
stack: [],
|
|
codeFrame: {
|
|
fileName,
|
|
location,
|
|
content: codeFrame,
|
|
},
|
|
message: {
|
|
content,
|
|
substitutions: [],
|
|
},
|
|
category: `${fileName} ${location}`,
|
|
};
|
|
}
|
|
|
|
export function parseLogBoxLog(
|
|
args: $ReadOnlyArray<mixed>,
|
|
): {|
|
|
componentStack: ComponentStack,
|
|
category: Category,
|
|
message: Message,
|
|
|} {
|
|
// This detects a very narrow case of a simple log string,
|
|
// with a component stack appended by React DevTools.
|
|
// In this case, we extract the component stack,
|
|
// because LogBox formats those pleasantly.
|
|
// If there are other substitutions or formatting,
|
|
// this could potentially corrupt the data, but there
|
|
// are currently no known cases of that happening.
|
|
let componentStack = [];
|
|
let argsWithoutComponentStack = [];
|
|
for (const arg of args) {
|
|
if (typeof arg === 'string' && /^\n {4}in/.exec(arg)) {
|
|
componentStack = parseComponentStack(arg);
|
|
} else {
|
|
argsWithoutComponentStack.push(arg);
|
|
}
|
|
}
|
|
|
|
return {
|
|
...parseCategory(argsWithoutComponentStack),
|
|
componentStack,
|
|
};
|
|
}
|