Skip to content

Commit

Permalink
[rust-compiler] add the extract-graphql crate
Browse files Browse the repository at this point in the history
This create is a simple, but extremely fast way to extract Relay
`graphql` tags from source files. It's a basic tokenizer for "js like"
languages including Typescript and Flow.
  • Loading branch information
kassens committed Feb 12, 2020
1 parent 7a77f31 commit 8c375a2
Show file tree
Hide file tree
Showing 16 changed files with 406 additions and 0 deletions.
7 changes: 7 additions & 0 deletions compiler/crates/extract-graphql/Cargo.toml
@@ -0,0 +1,7 @@
[project]
name = "extract-graphql"
version = "0.0.0"
edition = "2018"

[dev-dependencies]
fixture-tests = { path = "../fixture-tests" }
139 changes: 139 additions & 0 deletions compiler/crates/extract-graphql/src/lib.rs
@@ -0,0 +1,139 @@
/*
* 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.
*/

#![deny(warnings)]
#![deny(rust_2018_idioms)]
#![deny(clippy::all)]

use std::iter::Peekable;
use std::str::CharIndices;

type IndexedCharIter<'a> = Peekable<CharIndices<'a>>;

/// Extract graphql`text` literals from JS-like code. This should work for Flow
/// or TypeScript alike.
pub fn parse_chunks(input: &str) -> Result<Vec<&str>, String> {
if !input.contains("graphql`") {
return Ok(vec![]);
}
let mut res = vec![];
let mut it = input.char_indices().peekable();
'code: while let Some((i, c)) = it.next() {
match c {
'g' => {
for expected in ['r', 'a', 'p', 'h', 'q', 'l', '`'].iter() {
if let Some((_, c)) = it.next() {
if c != *expected {
consume_identifier(&mut it);
continue 'code;
}
}
}
let start = i;
while let Some((i, c)) = it.next() {
match c {
'`' => {
let end = i;
res.push(&input[start + 8..end]);
continue 'code;
}
'$' => {
if let Some((_, '{')) = it.next() {
return Err("graphql literals cannot have string substitutions."
.to_string());
}
}
_ => {}
}
}
}
'a'..='z' | 'A'..='Z' | '_' => {
consume_identifier(&mut it);
}
// Skip over template literals. Unfortunately, this isn't enough to
// deal with nested template literals and runs a risk of skipping
// over too much code.
// '`' => {
// while let Some((_, c)) = it.next() {
// match c {
// '`' => {
// continue 'code;
// }
// '\\' => {
// it.next();
// }
// _ => {}
// }
// }
// }
'"' => consume_string(&mut it, '"'),
'\'' => consume_string(&mut it, '\''),
'/' => match it.next() {
Some((_, '/')) => {
consume_line_comment(&mut it);
}
Some((_, '*')) => {
consume_block_comment(&mut it);
}
_ => {}
},
_ => {}
};
}
Ok(res)
}

fn consume_identifier(it: &mut IndexedCharIter<'_>) {
for (_, c) in it {
match c {
'a'..='z' | 'A'..='Z' | '_' | '0'..='9' => {}
_ => {
return;
}
}
}
}

fn consume_line_comment(it: &mut IndexedCharIter<'_>) {
for (_, c) in it {
match c {
'\n' | '\r' => {
break;
}
_ => {}
}
}
}

fn consume_block_comment(it: &mut IndexedCharIter<'_>) {
while let Some((_, c)) = it.next() {
if c == '*' {
if let Some((_, '/')) = it.peek() {
it.next();
break;
}
}
}
}

fn consume_string(it: &mut IndexedCharIter<'_>, quote: char) {
while let Some((_, c)) = it.next() {
match c {
'\\' => {
it.next();
}
'\'' | '"' if c == quote => {
return;
}
'\n' | '\r' => {
// Unexpected newline, terminate the string parsing to recover
return;
}
_ => {}
}
}
}
@@ -0,0 +1,25 @@
==================================== INPUT ====================================
/**
* 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.
*/

// graphql`ignored`

/*
graphql`ignored`
*/

/* ... */ graphql`not ignored 1`

/**
* block
**/
graphql`not ignored 2`
==================================== OUTPUT ===================================
[
"not ignored 1",
"not ignored 2",
]
19 changes: 19 additions & 0 deletions compiler/crates/extract-graphql/tests/extract/fixtures/comments.js
@@ -0,0 +1,19 @@
/**
* 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.
*/

// graphql`ignored`

/*
graphql`ignored`
*/

/* ... */ graphql`not ignored 1`

/**
* block
**/
graphql`not ignored 2`
@@ -0,0 +1,19 @@
==================================== INPUT ====================================
/**
* 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.
*/

// expected-to-throw

blah

graphql`
fragment Foo on ${type} {
__typename
}
`
==================================== ERROR ====================================
graphql literals cannot have string substitutions.
@@ -0,0 +1,16 @@
/**
* 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.
*/

// expected-to-throw

blah

graphql`
fragment Foo on ${type} {
__typename
}
`
@@ -0,0 +1,20 @@
==================================== INPUT ====================================
/**
* 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.
*/

const string = `
${line1 != null ? `${line1}\n` : ''}
${line2 != null ? `${line2}\n` : ''}
${city != null && state != null ? `${city}, ${state}\n` : ''}
${postalCode != null ? `${postalCode}\n` : ''}
`;

graphql`some graphql`;
==================================== OUTPUT ===================================
[
"some graphql",
]
@@ -0,0 +1,15 @@
/**
* 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.
*/

const string = `
${line1 != null ? `${line1}\n` : ''}
${line2 != null ? `${line2}\n` : ''}
${city != null && state != null ? `${city}, ${state}\n` : ''}
${postalCode != null ? `${postalCode}\n` : ''}
`;

graphql`some graphql`;
@@ -0,0 +1,22 @@
==================================== INPUT ====================================
/**
* 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.
*/

const oops = [
graphqlgraphql`not matching1`,
graphqlabc`not matching2`,
_graphql`not matching3`,
abcgraphql`not matching4`,
" graphql`in string` ",
" \" graphql`in string` ",
' graphql`in string` ',
' \' graphql`in string` ',
// graphql`in comment`
/* graphql`in comment` */
];
==================================== OUTPUT ===================================
[]
@@ -0,0 +1,19 @@
/**
* 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.
*/

const oops = [
graphqlgraphql`not matching1`,
graphqlabc`not matching2`,
_graphql`not matching3`,
abcgraphql`not matching4`,
" graphql`in string` ",
" \" graphql`in string` ",
' graphql`in string` ',
' \' graphql`in string` ',
// graphql`in comment`
/* graphql`in comment` */
];
@@ -0,0 +1,14 @@
==================================== INPUT ====================================
/**
* 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.
*/

<Test>'</Test>
graphql`fragment on User`
==================================== OUTPUT ===================================
[
"fragment on User",
]
@@ -0,0 +1,9 @@
/**
* 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.
*/

<Test>'</Test>
graphql`fragment on User`
@@ -0,0 +1,13 @@
==================================== INPUT ====================================
function MyComponent() {
useFragment(graphql`
fragment Test on User {
__typename
}
`, user)
return <div>Test</div>;
}
==================================== OUTPUT ===================================
[
"\n fragment Test on User {\n __typename\n }\n ",
]
@@ -0,0 +1,8 @@
function MyComponent() {
useFragment(graphql`
fragment Test on User {
__typename
}
`, user)
return <div>Test</div>;
}
13 changes: 13 additions & 0 deletions compiler/crates/extract-graphql/tests/extract/mod.rs
@@ -0,0 +1,13 @@
/*
* 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.
*/

use extract_graphql::parse_chunks;
use fixture_tests::Fixture;

pub fn transform_fixture(fixture: &Fixture) -> Result<String, String> {
parse_chunks(fixture.content).map(|chunks| format!("{:#?}", chunks))
}

0 comments on commit 8c375a2

Please sign in to comment.