Compare commits

...

2 Commits

Author SHA1 Message Date
Joe Savona
21be5d2812 [compiler] Add CompilerError.UnsupportedJS variant
We use this variant for syntax we intentionally don't support: with statements, eval, and inline class declarations.
2025-07-09 22:22:09 -07:00
Joe Savona
d221c0f565 [compiler] More precise errors for invalid import/export/namespace statements
import, export, and TS namespace statements can only be used at the top-level of a module, which is enforced by parsers already. Here we add a backup validation of that. As of this PR, we now have only major statement type (class declarations) listed as a todo.
2025-07-09 22:22:09 -07:00
4 changed files with 58 additions and 32 deletions

View File

@@ -15,6 +15,11 @@ export enum ErrorSeverity {
* misunderstanding on the users part.
*/
InvalidJS = 'InvalidJS',
/**
* JS syntax that is not supported and which we do not plan to support. Developers should
* rewrite to use supported forms.
*/
UnsupportedJS = 'UnsupportedJS',
/**
* Code that breaks the rules of React.
*/
@@ -241,12 +246,16 @@ export class CompilerError extends Error {
case ErrorSeverity.InvalidJS:
case ErrorSeverity.InvalidReact:
case ErrorSeverity.InvalidConfig:
case ErrorSeverity.UnsupportedJS: {
return true;
}
case ErrorSeverity.CannotPreserveMemoization:
case ErrorSeverity.Todo:
case ErrorSeverity.Todo: {
return false;
default:
}
default: {
assertExhaustive(detail.severity, 'Unhandled error severity');
}
}
});
}

View File

@@ -1359,7 +1359,7 @@ function lowerStatement(
builder.errors.push({
reason: `JavaScript 'with' syntax is not supported`,
description: `'with' syntax is considered deprecated and removed from JavaScript standards, consider alternatives`,
severity: ErrorSeverity.InvalidJS,
severity: ErrorSeverity.UnsupportedJS,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
@@ -1371,13 +1371,15 @@ function lowerStatement(
return;
}
case 'ClassDeclaration': {
/*
* We can in theory support nested classes, similarly to functions where we track values
* captured by the class and consider mutations of the instances to mutate the class itself
/**
* In theory we could support inline class declarations, but this is rare enough in practice
* and complex enough to support that we don't anticipate supporting anytime soon. Developers
* are encouraged to lift classes out of component/hook declarations.
*/
builder.errors.push({
reason: `Support nested class declarations`,
severity: ErrorSeverity.Todo,
reason: 'Inline `class` declarations are not supported',
description: `Move class declarations outside of components/hooks`,
severity: ErrorSeverity.UnsupportedJS,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
@@ -1397,6 +1399,41 @@ function lowerStatement(
});
return;
}
case 'ExportAllDeclaration':
case 'ExportDefaultDeclaration':
case 'ExportNamedDeclaration':
case 'ImportDeclaration':
case 'TSExportAssignment':
case 'TSImportEqualsDeclaration': {
builder.errors.push({
reason:
'JavaScript `import` and `export` statements may only appear at the top level of a module',
severity: ErrorSeverity.InvalidJS,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
return;
}
case 'TSNamespaceExportDeclaration': {
builder.errors.push({
reason:
'TypeScript `namespace` statements may only appear at the top level of a module',
severity: ErrorSeverity.InvalidJS,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
return;
}
case 'DeclareClass':
case 'DeclareExportAllDeclaration':
case 'DeclareExportDeclaration':
@@ -1411,32 +1448,12 @@ function lowerStatement(
case 'OpaqueType':
case 'TSDeclareFunction':
case 'TSInterfaceDeclaration':
case 'TSModuleDeclaration':
case 'TSTypeAliasDeclaration':
case 'TypeAlias': {
// We do not preserve type annotations/syntax through transformation
return;
}
case 'ExportAllDeclaration':
case 'ExportDefaultDeclaration':
case 'ExportNamedDeclaration':
case 'ImportDeclaration':
case 'TSExportAssignment':
case 'TSImportEqualsDeclaration':
case 'TSModuleDeclaration':
case 'TSNamespaceExportDeclaration': {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle ${stmtPath.type} statements`,
severity: ErrorSeverity.Todo,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
return;
}
default: {
return assertExhaustive(
stmtNode,
@@ -3545,7 +3562,7 @@ function lowerIdentifier(
reason: `The 'eval' function is not supported`,
description:
'Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler',
severity: ErrorSeverity.InvalidJS,
severity: ErrorSeverity.UnsupportedJS,
loc: exprPath.node.loc ?? null,
suggestions: null,
});

View File

@@ -15,7 +15,7 @@ function Component(props) {
```
1 | function Component(props) {
> 2 | eval('props.x = true');
| ^^^^ InvalidJS: The 'eval' function is not supported. Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler (2:2)
| ^^^^ UnsupportedJS: The 'eval' function is not supported. Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler (2:2)
3 | return <div />;
4 | }
5 |

View File

@@ -84,7 +84,7 @@ let moduleLocal = false;
> 3 | var x = [];
| ^^^^^^^^^^^ Todo: (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration (3:3)
Todo: Support nested class declarations (5:10)
UnsupportedJS: Inline `class` declarations are not supported. Move class declarations outside of components/hooks (5:10)
Todo: (BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement (20:22)