Skip to content
This repository has been archived by the owner on Aug 31, 2023. It is now read-only.

Commit

Permalink
feature(rome_js_semantic): extraction of reference (read) events (#2725)
Browse files Browse the repository at this point in the history
* extraction of read events
  • Loading branch information
xunilrj authored Jun 20, 2022
1 parent 48b03ff commit cf4189c
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 25 deletions.
122 changes: 102 additions & 20 deletions crates/rome_js_semantic/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
//! Events emitted by the [SemanticEventExtractor] which are then constructed into the Semantic Model
use std::collections::VecDeque;
use std::collections::{HashMap, VecDeque};

use rome_js_syntax::{JsLanguage, JsSyntaxNode, TextRange, TextSize};
use rome_rowan::syntax::Preorder;
use rome_js_syntax::{
JsIdentifierBinding, JsLanguage, JsReferenceIdentifier, JsSyntaxNode, JsSyntaxToken, TextRange,
TextSize,
};
use rome_rowan::{syntax::Preorder, SyntaxNodeCast, SyntaxTokenText};

/// Events emitted by the [SemanticEventExtractor]. These events are later
/// made into the Semantic Model.
#[derive(Debug)]
pub enum SemanticEvent {
/// Signifies that a new symbol declaration was found.
/// Currently is generated for:
/// Generated for:
/// - Variable Declarations
/// - Import bindings
/// - Functions parameters
Expand All @@ -19,14 +22,22 @@ pub enum SemanticEvent {
scope_started_at: TextSize,
},

/// Signifies that a symbol value is being read.
/// Generated for:
/// - All reference identifiers
Read {
range: TextRange,
declaration_at: Option<TextRange>,
},

/// Signifies that a new scope was started
/// Currently generated for:
/// Generated for:
/// - Blocks
/// - Function body
ScopeStarted { range: TextRange },

/// Signifies that a new scope was ended
/// Currently generated for:
/// Generated for:
/// - Blocks
/// - Function body
ScopeEnded {
Expand All @@ -41,6 +52,7 @@ impl SemanticEvent {
SemanticEvent::DeclarationFound { range, .. } => range,
SemanticEvent::ScopeStarted { range } => range,
SemanticEvent::ScopeEnded { range, .. } => range,
SemanticEvent::Read { range, .. } => range,
}
}

Expand Down Expand Up @@ -87,31 +99,57 @@ impl SemanticEvent {
pub struct SemanticEventExtractor {
stash: VecDeque<SemanticEvent>,
scopes: Vec<Scope>,
declared_names: HashMap<SyntaxTokenText, TextRange>,
}

struct ScopeDeclaration {
name: SyntaxTokenText,
}

struct Scope {
started_at: TextSize,
declared: Vec<ScopeDeclaration>,
shadowed: Vec<(SyntaxTokenText, TextRange)>,
}

impl SemanticEventExtractor {
pub fn new() -> Self {
Self {
stash: VecDeque::new(),
scopes: vec![],
declared_names: HashMap::new(),
}
}

/// See [SemanticEvent] for a more detailed description
/// of which ```SyntaxNode``` generates which events.
pub fn enter(&mut self, node: &JsSyntaxNode) {
use rome_js_syntax::JsSyntaxKind::*;
use SemanticEvent::*;

match node.kind() {
JS_IDENTIFIER_BINDING => self.stash.push_back(DeclarationFound {
range: node.text_range(),
scope_started_at: self.current_scope_start(),
}),
JS_IDENTIFIER_BINDING => {
if let Some(name_token) = node
.clone()
.cast::<JsIdentifierBinding>()
.and_then(|id| id.name_token().ok())
{
self.declare_name(&name_token);
}
}
JS_REFERENCE_IDENTIFIER => {
if let Some(name_token) = node
.clone()
.cast::<JsReferenceIdentifier>()
.and_then(|reference| reference.value_token().ok())
{
self.stash.push_back(SemanticEvent::Read {
range: node.text_range(),
declaration_at: self
.get_declaration_range_by_trimmed_text(&name_token)
.cloned(),
})
}
}

JS_MODULE | JS_SCRIPT => self.push_scope(node.text_range()),
JS_FUNCTION_DECLARATION
Expand Down Expand Up @@ -155,15 +193,6 @@ impl SemanticEventExtractor {
}
}

fn current_scope_start(&self) -> TextSize {
let started_at = self.scopes.last().map(|x| x.started_at);

// We should always have, at least, the global scope
debug_assert!(started_at.is_some());

started_at.unwrap_or_else(|| TextSize::of(""))
}

/// Return any previous extracted [SemanticEvent].
pub fn pop(&mut self) -> Option<SemanticEvent> {
self.stash.pop_front()
Expand All @@ -173,6 +202,8 @@ impl SemanticEventExtractor {
self.stash.push_back(SemanticEvent::ScopeStarted { range });
self.scopes.push(Scope {
started_at: range.start(),
declared: vec![],
shadowed: vec![],
});
}

Expand All @@ -182,8 +213,59 @@ impl SemanticEventExtractor {
range,
started_at: scope.started_at,
});

// remove all declarations
for decl in scope.declared {
self.declared_names.remove(&decl.name);
}

// return all shadowed names
for (name, range) in scope.shadowed {
self.declared_names.insert(name, range);
}
}
}

fn current_scope_mut(&mut self) -> &mut Scope {
// We should at least have the global scope
debug_assert!(!self.scopes.is_empty());

match self.scopes.last_mut() {
None => unreachable!(),
Some(scope) => scope,
}
}

fn declare_name(&mut self, name_token: &JsSyntaxToken) {
let name = name_token.token_text_trimmed();

let declaration_range = name_token.text_range();

// insert this name into the list of available names
// and save shadowed names to be used later
let shadowed = self
.declared_names
.insert(name.clone(), declaration_range)
.map(|shadowed_range| (name.clone(), shadowed_range));

let current_scope = self.current_scope_mut();
current_scope.declared.push(ScopeDeclaration { name });
current_scope.shadowed.extend(shadowed);
let scope_started_at = current_scope.started_at;

self.stash.push_back(SemanticEvent::DeclarationFound {
range: declaration_range,
scope_started_at,
});
}

fn get_declaration_range_by_trimmed_text(
&self,
name_token: &JsSyntaxToken,
) -> Option<&TextRange> {
let name = name_token.token_text_trimmed();
self.declared_names.get(&name)
}
}

/// Extracts [SemanticEvent] from [SyntaxNode].
Expand Down
77 changes: 76 additions & 1 deletion crates/rome_js_semantic/src/tests/assertions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ use std::collections::{BTreeMap, HashMap};
/// let a/*#A*/ = 1;
/// ```
///
/// #### Read Assertion
///
/// Test if the attached token is reference "reading" the value of a symbol.
/// Pattern: ```/*READ <LABEL> */
///
/// /// Example:
/// ```js
/// let a/*#A*/ = 1;
/// let b = a/*READ A*/ + 1;
/// ```
///
/// #### At Scope Assertion
///
/// Test if the attached token is a declaration that lives inside the specified scope.
Expand Down Expand Up @@ -110,6 +121,10 @@ pub fn assert(code: &str, test_name: &str) {
let v = events_by_pos.entry(range.end()).or_default();
v.push(event);
}
SemanticEvent::Read { range, .. } => {
let v = events_by_pos.entry(range.start()).or_default();
v.push(event);
}
}
}

Expand All @@ -126,6 +141,12 @@ struct DeclarationAssertion {
declaration_name: String,
}

#[derive(Clone, Debug)]
struct ReadAssertion {
range: TextRange,
symbol_name: String,
}

#[derive(Clone, Debug)]
struct AtScopeAssertion {
range: TextRange,
Expand Down Expand Up @@ -157,6 +178,7 @@ struct UniqueAssertion {
#[derive(Clone, Debug)]
enum SemanticAssertion {
Declaration(DeclarationAssertion),
Read(ReadAssertion),
ScopeStart(ScopeStartAssertion),
ScopeEnd(ScopeEndAssertion),
AtScope(AtScopeAssertion),
Expand All @@ -178,6 +200,18 @@ impl SemanticAssertion {
range: token.text_range(),
declaration_name: name,
}))
} else if assertion_text.starts_with("/*READ ") {
let symbol_name = assertion_text
.trim()
.trim_start_matches("/*READ ")
.trim_end_matches("*/")
.trim()
.to_string();

Some(SemanticAssertion::Read(ReadAssertion {
range: token.text_range(),
symbol_name,
}))
} else if assertion_text.contains("/*START") {
let scope_name = assertion_text
.trim()
Expand Down Expand Up @@ -228,6 +262,7 @@ impl SemanticAssertion {
#[derive(Debug)]
struct SemanticAssertions {
declarations_assertions: BTreeMap<String, DeclarationAssertion>,
read_assertions: Vec<ReadAssertion>,
at_scope_assertions: Vec<AtScopeAssertion>,
scope_start_assertions: BTreeMap<String, ScopeStartAssertion>,
scope_end_assertions: Vec<ScopeEndAssertion>,
Expand All @@ -238,6 +273,7 @@ struct SemanticAssertions {
impl SemanticAssertions {
fn from_root(root: JsAnyRoot, code: &str, test_name: &str) -> Self {
let mut declarations_assertions: BTreeMap<String, DeclarationAssertion> = BTreeMap::new();
let mut read_assertions = vec![];
let mut at_scope_assertions = vec![];
let mut scope_start_assertions: BTreeMap<String, ScopeStartAssertion> = BTreeMap::new();
let mut scope_end_assertions = vec![];
Expand Down Expand Up @@ -267,6 +303,9 @@ impl SemanticAssertions {
error_assertion_name_clash(&token, code, test_name, old);
}
}
Some(SemanticAssertion::Read(assertion)) => {
read_assertions.push(assertion);
}
Some(SemanticAssertion::ScopeStart(assertion)) => {
// Scope start assertions names cannot clash
let old = scope_start_assertions
Expand All @@ -288,14 +327,16 @@ impl SemanticAssertions {
Some(SemanticAssertion::Unique(assertion)) => {
uniques.push(assertion);
}
_ => {}

None => {}
};
}
}
}

Self {
declarations_assertions,
read_assertions,
at_scope_assertions,
scope_start_assertions,
scope_end_assertions,
Expand Down Expand Up @@ -329,6 +370,40 @@ impl SemanticAssertions {
}
}

// Check every read assertion is ok

for assertion in self.read_assertions.iter() {
let decl = match self.declarations_assertions.get(&assertion.symbol_name) {
Some(decl) => decl,
None => {
panic!("No declaration found with name: {}", assertion.symbol_name);
}
};

let events = match events_by_pos.get(&assertion.range.start()) {
Some(events) => events,
None => {
panic!("No read event found at this range");
}
};

let at_least_one_match = dbg!(events).iter().any(|e| {
if let SemanticEvent::Read {
declaration_at: Some(declaration_at_range),
..
} = e
{
code[*declaration_at_range] == code[decl.range]
} else {
false
}
});

if !at_least_one_match {
panic!("No matching read event found at this range");
}
}

// Check every at scope assertion is ok

for assertion in self.at_scope_assertions.iter() {
Expand Down
1 change: 1 addition & 0 deletions crates/rome_js_semantic/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod assertions;
pub mod declarations;
mod references;
pub mod scopes;

#[macro_export]
Expand Down
17 changes: 17 additions & 0 deletions crates/rome_js_semantic/src/tests/references.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use crate::assert_semantics;

assert_semantics! {
ok_reference_read_global, "let a/*#A*/ = 1; let b = a/*READ A*/ + 1;",
ok_reference_read_inner_scope, r#"function f(a/*#A1*/) {
let b = a/*READ A1*/ + 1;
console.log(b);
if (true) {
let a/*#A2*/ = 2;
let b = a/*READ A2*/ + 1;
console.log(b);
}
let c = a/*READ A1*/ + 1;
console.log(b);
}
f(1);"#,
}
8 changes: 8 additions & 0 deletions crates/rome_rowan/src/cursor/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ impl SyntaxToken {
SyntaxTokenText::new(self.green().to_owned())
}

#[inline]
pub fn token_text_trimmed(&self) -> SyntaxTokenText {
let green = self.green().to_owned();
let mut range = self.text_trimmed_range();
range -= self.data().offset;
SyntaxTokenText::with_range(green, range)
}

#[inline]
pub fn text_trimmed(&self) -> &str {
self.green().text_trimmed()
Expand Down
Loading

0 comments on commit cf4189c

Please sign in to comment.