[rust][sema] For statements and JSX

Adds semantic analysis support for normal `for` statements and for JSX. The main 
catch with JSX is that there are a bunch of identifiers that we have to ignore 
since they aren't variable references: jsx attribute names, namespace names, jsx 
member expression properties, and closing elements.
This commit is contained in:
Joe Savona
2023-08-04 12:02:21 -07:00
parent 4756c5ab26
commit 3df04725bd
7 changed files with 1460 additions and 85 deletions
@@ -1,5 +1,8 @@
// Manual extensions to generated types
use crate::{Pattern, SourceRange, SourceType};
use crate::{
JSXElementName, JSXMemberExpression, JSXMemberExpressionOrIdentifier, Pattern, SourceRange,
SourceType,
};
/// Sentinel trait to distinguish AST *node* types
pub trait ESTreeNode {}
@@ -21,3 +24,22 @@ impl Pattern {
}
}
}
impl JSXElementName {
pub fn root_name(&self) -> &str {
match self {
Self::JSXIdentifier(name) => &name.name,
Self::JSXMemberExpression(name) => name.root_name(),
Self::JSXNamespacedName(name) => &name.namespace.name,
}
}
}
impl JSXMemberExpression {
pub fn root_name(&self) -> &str {
match &self.object {
JSXMemberExpressionOrIdentifier::JSXMemberExpression(object) => object.root_name(),
JSXMemberExpressionOrIdentifier::JSXIdentifier(object) => &object.name,
}
}
}
@@ -1,8 +1,8 @@
use forget_diagnostics::Diagnostic;
use forget_estree::{
AssignmentOperator, AssignmentPropertyOrRestElement, AssignmentTarget, Expression,
ExpressionOrSuper, ForInInit, Function, FunctionBody, Identifier, Pattern, Program,
SourceRange, SourceType, Statement, VariableDeclarationKind, Visitor2,
ExpressionOrSuper, ForInInit, ForInit, Function, FunctionBody, Identifier, JSXElementName,
Pattern, Program, SourceRange, SourceType, Statement, VariableDeclarationKind, Visitor2,
};
use crate::{AstNode, DeclarationKind, LabelKind, ReferenceKind, ScopeId, ScopeKind, ScopeManager};
@@ -190,7 +190,7 @@ impl Analyzer {
left: &ForInInit,
right: &Expression,
body: &Statement,
range: Option<SourceRange>,
_range: Option<SourceRange>,
) {
// Record an anonymous label for the statement to resolve unlabeled break/continue
let label = self
@@ -455,17 +455,31 @@ impl Visitor2 for Analyzer {
);
}
fn visit_identifier(&mut self, ast: &forget_estree::Identifier) {
Analyzer::visit_reference_identifier(
self,
&ast.name,
AstNode::from(ast),
ReferenceKind::Read,
ast.range,
);
fn visit_for_statement(&mut self, ast: &forget_estree::ForStatement) {
let mut for_scope: Option<ScopeId> = None;
if let Some(init) = &ast.init {
if let ForInit::VariableDeclaration(init) = init {
if init.kind != VariableDeclarationKind::Var {
for_scope = Some(self.enter_scope(ScopeKind::For));
}
}
}
if let Some(init) = &ast.init {
self.visit_for_init(init);
}
if let Some(test) = &ast.test {
self.visit_expression(test);
}
if let Some(update) = &ast.update {
self.visit_expression(update);
}
self.visit_statement(&ast.body);
if let Some(for_scope) = for_scope {
self.close_scope(for_scope);
}
}
fn visit_jsxidentifier(&mut self, ast: &forget_estree::JSXIdentifier) {
fn visit_identifier(&mut self, ast: &forget_estree::Identifier) {
Analyzer::visit_reference_identifier(
self,
&ast.name,
@@ -561,4 +575,82 @@ impl Visitor2 for Analyzer {
}
}
}
fn visit_jsxattribute(&mut self, ast: &forget_estree::JSXAttribute) {
// NOTE: skip visiting the attribute name, attributes are like non-computed
// object properties where the identifier is not a variable reference
if let Some(value) = &ast.value {
self.visit_jsxattribute_value(value);
}
}
fn visit_jsxclosing_element(&mut self, _ast: &forget_estree::JSXClosingElement) {
// no-op, should not be counted as a reference
}
fn visit_jsxidentifier(&mut self, ast: &forget_estree::JSXIdentifier) {
Analyzer::visit_reference_identifier(
self,
&ast.name,
AstNode::from(ast),
ReferenceKind::Read,
ast.range,
);
}
fn visit_jsxfragment(&mut self, ast: &forget_estree::JSXFragment) {
// TODO: record the pragmas
for child in &ast.children {
self.visit_jsxchild_item(child);
}
}
fn visit_jsxmember_expression(&mut self, ast: &forget_estree::JSXMemberExpression) {
// NOTE: ignore the 'property' since JSX doesn't support computed properties
self.visit_jsxmember_expression_or_identifier(&ast.object);
}
fn visit_jsxnamespaced_name(&mut self, ast: &forget_estree::JSXNamespacedName) {
// NOTE: ignore the 'name' since it doesn't refer to a variable
self.visit_jsxidentifier(&ast.namespace);
}
fn visit_jsxopening_element(&mut self, ast: &forget_estree::JSXOpeningElement) {
// TODO: record jsx pragma if root_name is not an FBT name
let root_name = ast.name.root_name();
match &ast.name {
JSXElementName::JSXIdentifier(name) => {
// lowercase names are builtins, only visit if this is a user-defined
// component
if let Some(first) = root_name.chars().next() {
if first == first.to_ascii_uppercase() {
self.visit_jsxidentifier(name);
}
} else {
// TODO: this likely indicates a parse error, since a valid parse
// should never result in an empty JSXIdentifier node. but just in
// case we report this rather than silently fail
self.manager.diagnostics.push(Diagnostic::invalid_syntax(
"Expected JSXOpenintElement.name to be non-empty",
name.range,
));
}
}
JSXElementName::JSXMemberExpression(name) => {
if root_name != "this" {
self.visit_jsxmember_expression(name);
}
}
JSXElementName::JSXNamespacedName(name) => {
if root_name != "this" {
self.visit_jsxnamespaced_name(name);
}
}
}
for attribute in &ast.attributes {
self.visit_jsxattribute_or_spread(attribute);
}
}
}
@@ -65,6 +65,10 @@ impl ScopeManager {
}
}
pub fn diagnostics(&mut self) -> Vec<Diagnostic> {
std::mem::take(&mut self.diagnostics)
}
pub fn root(&self) -> &Scope {
&self.scopes[self.root.0]
}
@@ -1,3 +1,5 @@
use std::fmt::Write;
use forget_hermes_parser::parse;
use forget_semantic_analysis::analyze;
use insta::{assert_snapshot, glob};
@@ -8,12 +10,17 @@ fn fixtures() {
println!("fixture {}", path.to_str().unwrap());
let input = std::fs::read_to_string(path).unwrap();
let ast = parse(&input, path.to_str().unwrap()).unwrap();
let analysis = analyze(&ast);
let mut analysis = analyze(&ast);
let ast_output = serde_json::to_string_pretty(&ast).unwrap();
let analysis_output = format!("{:#?}", analysis.debug());
let mut output = String::new();
writeln!(&mut output, "{:#?}", analysis.debug()).unwrap();
let diagnostics = analysis.diagnostics();
for diagnostic in diagnostics {
writeln!(&mut output, "{:#?}", diagnostic).unwrap();
}
assert_snapshot!(format!(
"Input:\n{input}\n\nAST:\n{ast_output}\n\nAnalysis:\n{analysis_output}"
"Input:\n{input}\n\nAST:\n{ast_output}\n\nAnalysis:\n{output}"
));
});
}
@@ -0,0 +1,17 @@
import Foo from "foo";
import * as Bar from "bar";
import { Baz } from "baz";
function Component(props) {
let g = global;
let y = new Array(props.count);
let s = String("hello");
let b = Boolean(true);
let n = Number(0);
let x = Math.min(props.x, props.y);
setTimeout(() => {}, 0);
setInterval(() => {}, 0);
Foo;
Bar;
Baz;
}
@@ -1,6 +1,6 @@
---
source: crates/forget_semantic_analysis/tests/analysis_test.rs
expression: "format!(\"Input:\\n{input}\\n\\nAST:\\n{ast_output}\\n\\nAnalysis:\\n{analysis_output}\")"
expression: "format!(\"Input:\\n{input}\\n\\nAST:\\n{ast_output}\\n\\nAnalysis:\\n{output}\")"
input_file: crates/forget_semantic_analysis/tests/fixtures/labels.js
---
Input:
@@ -517,43 +517,8 @@ Scope {
2,
),
},
"x": Declaration {
id: DeclarationId(
3,
),
kind: Let,
scope: ScopeId(
2,
),
},
},
references: [
Reference {
id: ReferenceId(
0,
),
kind: Read,
declaration: DeclarationId(
3,
),
declaration (name): "x",
scope: ScopeId(
2,
),
},
Reference {
id: ReferenceId(
1,
),
kind: Read,
declaration: DeclarationId(
3,
),
declaration (name): "x",
scope: ScopeId(
2,
),
},
Reference {
id: ReferenceId(
6,
@@ -573,13 +538,23 @@ Scope {
id: ScopeId(
3,
),
kind: Block,
kind: For,
labels: {},
declarations: {},
declarations: {
"x": Declaration {
id: DeclarationId(
3,
),
kind: Let,
scope: ScopeId(
3,
),
},
},
references: [
Reference {
id: ReferenceId(
2,
0,
),
kind: Read,
declaration: DeclarationId(
@@ -592,20 +567,7 @@ Scope {
},
Reference {
id: ReferenceId(
3,
),
kind: ReadWrite,
declaration: DeclarationId(
2,
),
declaration (name): "y",
scope: ScopeId(
3,
),
},
Reference {
id: ReferenceId(
4,
1,
),
kind: Read,
declaration: DeclarationId(
@@ -616,19 +578,6 @@ Scope {
3,
),
},
Reference {
id: ReferenceId(
5,
),
kind: Read,
declaration: DeclarationId(
2,
),
declaration (name): "y",
scope: ScopeId(
3,
),
},
],
children: [
Scope {
@@ -638,14 +587,78 @@ Scope {
kind: Block,
labels: {},
declarations: {},
references: [],
children: [],
references: [
Reference {
id: ReferenceId(
2,
),
kind: Read,
declaration: DeclarationId(
3,
),
declaration (name): "x",
scope: ScopeId(
4,
),
},
Reference {
id: ReferenceId(
3,
),
kind: ReadWrite,
declaration: DeclarationId(
2,
),
declaration (name): "y",
scope: ScopeId(
4,
),
},
Reference {
id: ReferenceId(
4,
),
kind: Read,
declaration: DeclarationId(
3,
),
declaration (name): "x",
scope: ScopeId(
4,
),
},
Reference {
id: ReferenceId(
5,
),
kind: Read,
declaration: DeclarationId(
2,
),
declaration (name): "y",
scope: ScopeId(
4,
),
},
],
children: [
Scope {
id: ScopeId(
5,
),
kind: Block,
labels: {},
declarations: {},
references: [],
children: [],
},
],
},
],
},
Scope {
id: ScopeId(
5,
6,
),
kind: Block,
labels: {},
@@ -659,3 +672,4 @@ Scope {
},
],
}