diff --git a/.changeset/ten-pianos-report.md b/.changeset/ten-pianos-report.md new file mode 100644 index 00000000000..970d1b999a7 --- /dev/null +++ b/.changeset/ten-pianos-report.md @@ -0,0 +1,6 @@ +--- +"graphql-language-service-server": patch +"graphql-language-service": patch +--- + +Better handling of unparsable babel JS/TS files diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index 58c694faa2f..2af5873bb6d 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -127,7 +127,7 @@ export class MessageProcessor { this._graphQLConfig = config; this._parser = (text, uri) => { const p = parser ?? parseDocument; - return p(text, uri, fileExtensions, graphqlFileExtensions); + return p(text, uri, fileExtensions, graphqlFileExtensions, this._logger); }; this._tmpDir = tmpDir || tmpdir(); this._tmpDirBase = path.join(this._tmpDir, 'graphql-language-service'); diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts index 0b7749f64c0..c3025cf7f53 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts @@ -623,6 +623,26 @@ export function Example(arg: string) {}`; expect(contents.length).toEqual(0); }); + it('an unparsable JS/TS file does not throw and bring down the server', async () => { + const text = ` +// @flow +import type randomthing fro 'package'; +import type {B} from 'B'; +im port A from './A'; + +con QUERY = randomthing\` +query Test { + test { + value + ...FragmentsComment + } +} +\${A.frag`; + + const contents = parseDocument(text, 'test.js'); + expect(contents.length).toEqual(0); + }); + describe('handleWatchedFilesChangedNotification', () => { const mockReadFileSync: jest.Mock = jest.requireMock('fs').readFileSync; diff --git a/packages/graphql-language-service-server/src/findGraphQLTags.ts b/packages/graphql-language-service-server/src/findGraphQLTags.ts index f8aef188645..b016450dc66 100644 --- a/packages/graphql-language-service-server/src/findGraphQLTags.ts +++ b/packages/graphql-language-service-server/src/findGraphQLTags.ts @@ -17,6 +17,7 @@ import { import { Position, Range } from 'graphql-language-service-utils'; import { parse, ParserOptions, ParserPlugin } from '@babel/parser'; +import { Logger } from './Logger'; // Attempt to be as inclusive as possible of source text. const PARSER_OPTIONS: ParserOptions = { @@ -68,18 +69,36 @@ const BABEL_PLUGINS: ParserPlugin[] = [ 'logicalAssignment', ]; -export function findGraphQLTags(text: string, ext: string): TagResult[] { +export function findGraphQLTags( + text: string, + ext: string, + uri: string, + logger: Logger, +): TagResult[] { const result: TagResult[] = []; const plugins = BABEL_PLUGINS.slice(0, BABEL_PLUGINS.length); - if (ext === '.ts' || ext === '.tsx') { + const isTypeScript = ext === '.ts' || ext === '.tsx'; + if (isTypeScript) { plugins?.push('typescript'); } else { plugins?.push('flow', 'flowComments'); } PARSER_OPTIONS.plugins = plugins; - const ast = parse(text, PARSER_OPTIONS); + + let parsedAST: ReturnType | undefined = undefined; + try { + parsedAST = parse(text, PARSER_OPTIONS); + } catch (error) { + const type = isTypeScript ? 'TypeScript' : 'JavaScript'; + logger.error( + `Could not parse the ${type} file at ${uri} to extract the graphql tags:`, + ); + logger.error(error); + return []; + } + const ast = parsedAST!; const visitors = { CallExpression: (node: Expression) => { diff --git a/packages/graphql-language-service-server/src/parseDocument.ts b/packages/graphql-language-service-server/src/parseDocument.ts index 4c65c7f60ad..a89255f8b7d 100644 --- a/packages/graphql-language-service-server/src/parseDocument.ts +++ b/packages/graphql-language-service-server/src/parseDocument.ts @@ -3,6 +3,7 @@ import type { CachedContent } from 'graphql-language-service'; import { Range, Position } from 'graphql-language-service-utils'; import { findGraphQLTags, DEFAULT_TAGS } from './findGraphQLTags'; +import { Logger } from './Logger'; export const DEFAULT_SUPPORTED_EXTENSIONS = [ '.js', @@ -17,7 +18,7 @@ export const DEFAULT_SUPPORTED_EXTENSIONS = [ ]; /** - * .graphql is the officially reccomended extension for graphql files + * .graphql is the officially recommended extension for graphql files * * .gql and .graphqls are included for compatibility for commonly used extensions * @@ -43,6 +44,7 @@ export function parseDocument( uri: string, fileExtensions: string[] = DEFAULT_SUPPORTED_EXTENSIONS, graphQLFileExtensions: string[] = DEFAULT_SUPPORTED_GRAPHQL_EXTENSIONS, + logger: Logger = new Logger(), ): CachedContent[] { // Check if the text content includes a GraphQLV query. // If the text doesn't include GraphQL queries, do not proceed. @@ -51,7 +53,7 @@ export function parseDocument( if (DEFAULT_TAGS.some(t => t === text)) { return []; } - const templates = findGraphQLTags(text, ext); + const templates = findGraphQLTags(text, ext, uri, logger); return templates.map(({ template, range }) => ({ query: template, range })); } if (graphQLFileExtensions.some(e => e === ext)) {