Skip to content

Commit cbbf439

Browse files
Swatinemloewenheim
andauthored
ref: Switch JS parser from rslint to swc (#13)
The rslint parser is unmaintained and has a hard limit on how much code it can parse. This changes the internal parser so swc, which is maintained and production ready. Instead of having untyped SyntaxNodes that allow free navigation within the hierarchy, swc is rather based around typed AST nodes and a visitor pattern. Some more code patterns are being covered by this change. Last but not least, Scope extraction is now a fallible operation which is a breaking change. Co-authored-by: Sebastian Zivota <[email protected]>
1 parent 9bc019c commit cbbf439

File tree

9 files changed

+735
-59
lines changed

9 files changed

+735
-59
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Features
6+
7+
- Handle getters, setters, non-identifier property names, and object literals in scope names. ([#13](https://github.com/getsentry/js-source-scopes/pull/13))
8+
9+
### Fixes
10+
11+
- `extract_scope_names` now returns an error if parsing fails, instead of an empty vector of scopes. ([#13](https://github.com/getsentry/js-source-scopes/pull/13))
12+
13+
### Internal
14+
15+
- Switch JS parser from rslint to swc, which is actively maintained. ([#13](https://github.com/getsentry/js-source-scopes/pull/13))
16+
317
## 0.1.0
418

519
Inception

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ homepage = "https://github.com/getsentry/js-source-scopes"
1212
repository = "https://github.com/getsentry/js-source-scopes"
1313

1414
[dependencies]
15-
#swc_ecma_parser = "0.103.0"
15+
swc_common = "0.29.5"
16+
swc_ecma_parser = "0.122.6"
17+
swc_ecma_visit = { version = "0.80.5", features = ["path"] }
1618
indexmap = "1.7.0"
17-
rslint_parser = "0.3.1"
1819
sourcemap = "6.0.2"
1920
thiserror = "1.0.32"
2021
tracing = "0.1.36"

src/lib.rs

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
#![doc = include_str!("../README.md")]
22
#![warn(missing_docs)]
33

4+
use std::fmt::Display;
45
use std::ops::Range;
56

67
mod name_resolver;
7-
mod rslint;
88
mod scope_index;
99
mod scope_name;
1010
mod source;
11+
mod swc;
1112

1213
pub use name_resolver::NameResolver;
1314
pub use scope_index::{ScopeIndex, ScopeIndexError, ScopeLookupResult};
1415
pub use scope_name::{NameComponent, ScopeName};
1516
pub use source::{SourceContext, SourceContextError, SourcePosition};
17+
use swc_common::Spanned;
18+
19+
/// The Scopes extracted from a piece of JS Code.
20+
pub type Scopes = Vec<(Range<u32>, Option<ScopeName>)>;
1621

1722
/// Extracts function scopes from the given JS-like `src`.
1823
///
@@ -34,6 +39,7 @@ pub use source::{SourceContext, SourceContextError, SourcePosition};
3439
/// let src = "const arrowFnExpr = (a) => a; function namedFnDecl() {}";
3540
/// // arrowFnExpr -^------^ ^------namedFnDecl------^
3641
/// let mut scopes: Vec<_> = js_source_scopes::extract_scope_names(src)
42+
/// .unwrap()
3743
/// .into_iter()
3844
/// .map(|res| {
3945
/// let components = res.1.map(|n| n.components().map(|c| {
@@ -50,31 +56,37 @@ pub use source::{SourceContext, SourceContextError, SourcePosition};
5056
/// assert_eq!(scopes, expected);
5157
/// ```
5258
#[tracing::instrument(level = "trace", skip_all)]
53-
pub fn extract_scope_names(src: &str) -> Vec<(Range<u32>, Option<ScopeName>)> {
54-
rslint::parse_with_rslint(src)
55-
}
59+
pub fn extract_scope_names(src: &str) -> Result<Scopes, ParseError> {
60+
let mut scopes = swc::parse_with_swc(src).map_err(|e| ParseError { inner: e })?;
5661

57-
// TODO: maybe see if swc makes scope extraction easier / faster ?
58-
/*mod swc {
59-
use swc_ecma_parser::lexer::Lexer;
60-
use swc_ecma_parser::{Parser, StringInput, TsConfig};
61-
62-
pub fn parse_with_swc(src: &str) {
63-
swc_ecma_parser::parse_file_as_module();
62+
// filter out empty names
63+
for scope in &mut scopes {
64+
if let Some(ref name) = scope.1 {
65+
if name.components.is_empty() {
66+
scope.1 = None;
67+
}
68+
}
69+
}
6470

65-
let source = SourceFile;
71+
Ok(scopes)
72+
}
6673

67-
let mut parser = Parser::new(
68-
swc_ecma_parser::Syntax::Typescript(TsConfig {
69-
tsx: true,
70-
decorators: true,
71-
dts: true,
72-
no_early_errors: true,
73-
}),
74-
StringInput::from(src),
75-
None,
76-
);
74+
/// An error parsing the JS Source provided to [`extract_scope_names`].
75+
#[derive(Debug)]
76+
pub struct ParseError {
77+
inner: swc::ParseError,
78+
}
7779

78-
let module = parser.parse_module().unwrap();
80+
impl Display for ParseError {
81+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82+
let span = self.inner.span();
83+
f.write_fmt(format_args!(
84+
"{}:{}:{}",
85+
span.lo.0,
86+
span.hi.0,
87+
self.inner.kind().msg()
88+
))
7989
}
80-
}*/
90+
}
91+
92+
impl std::error::Error for ParseError {}

src/scope_name.rs

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
use std::borrow::Cow;
12
use std::collections::VecDeque;
23
use std::fmt::Display;
34
use std::ops::Range;
45

5-
use rslint_parser::SyntaxToken;
6+
use swc_ecma_visit::swc_ecma_ast as ast;
67

7-
use crate::rslint::convert_text_range;
8+
use crate::swc::convert_span;
89

910
/// An abstract scope name which can consist of multiple [`NameComponent`]s.
1011
#[derive(Debug)]
@@ -45,8 +46,7 @@ impl NameComponent {
4546
pub fn text(&self) -> &str {
4647
match &self.inner {
4748
NameComponentInner::Interpolation(s) => s,
48-
NameComponentInner::SourceIdentifierToken(t) => t.text().as_str(),
49-
NameComponentInner::SourcePunctuationToken(_) => "",
49+
NameComponentInner::SourceIdentifierToken(t) => &t.sym,
5050
}
5151
}
5252

@@ -56,34 +56,25 @@ impl NameComponent {
5656
/// to a specific token inside the source text.
5757
pub fn range(&self) -> Option<Range<u32>> {
5858
match &self.inner {
59-
NameComponentInner::SourceIdentifierToken(t)
60-
| NameComponentInner::SourcePunctuationToken(t) => {
61-
Some(convert_text_range(t.text_range()))
62-
}
59+
NameComponentInner::SourceIdentifierToken(t) => Some(convert_span(t.span)),
6360
_ => None,
6461
}
6562
}
6663

67-
pub(crate) fn interp(s: &'static str) -> Self {
64+
pub(crate) fn interp(s: impl Into<Cow<'static, str>>) -> Self {
6865
Self {
69-
inner: NameComponentInner::Interpolation(s),
66+
inner: NameComponentInner::Interpolation(s.into()),
7067
}
7168
}
72-
pub(crate) fn ident(token: SyntaxToken) -> Self {
69+
pub(crate) fn ident(ident: ast::Ident) -> Self {
7370
Self {
74-
inner: NameComponentInner::SourceIdentifierToken(token),
75-
}
76-
}
77-
pub(crate) fn punct(token: SyntaxToken) -> Self {
78-
Self {
79-
inner: NameComponentInner::SourcePunctuationToken(token),
71+
inner: NameComponentInner::SourceIdentifierToken(ident),
8072
}
8173
}
8274
}
8375

8476
#[derive(Debug)]
8577
pub(crate) enum NameComponentInner {
86-
Interpolation(&'static str),
87-
SourceIdentifierToken(SyntaxToken),
88-
SourcePunctuationToken(SyntaxToken),
78+
Interpolation(Cow<'static, str>),
79+
SourceIdentifierToken(ast::Ident),
8980
}

0 commit comments

Comments
 (0)