Slide 1

Slide 1 text

Backside of TypeScript language service plugin #tsc_api_study

Slide 2

Slide 2 text

Background - https://github.com/Quramy/ts-graphql-plugin - Language Service Plugin for GraphQL Query /* tsconfig.json */
 
 { "compilerOptions": { "plugins": [ { "name": "ts-graphql-plugin", // plugin NPM package name "schema": "schema.graphql", "tag": "gql" } ] } }

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Related TS API - ts-graphql-plugin features: - As Language Service Plugin - Auto complete, Query validation, Quick info, etc... - As CLI: - Generate .d.ts files for GraphQL query, validation command, etc... - As webpack plugin: - AoT Query compilation via custom transformer API

Slide 5

Slide 5 text

Do you know LS APIs ? interface LanguageService { cleanupSemanticCache(): void; getSyntacticDiagnostics(fileName: string): DiagnosticWithLocation[]; /** The first time this is called, it will return global diagnostics (no location). */ getSemanticDiagnostics(fileName: string): Diagnostic[]; getSuggestionDiagnostics(fileName: string): DiagnosticWithLocation[]; getCompilerOptionsDiagnostics(): Diagnostic[]; /** * @deprecated Use getEncodedSyntacticClassifications instead. */ getSyntacticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[]; /** * @deprecated Use getEncodedSemanticClassifications instead. */ getSemanticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[]; getEncodedSyntacticClassifications(fileName: string, span: TextSpan): Classifications; getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications; getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined): WithMetadata | undefined; getCompletionEntryDetails(fileName: string, position: number, name: string, formatOptions: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences | undefined): CompletionEntryDetails | undefined; getCompletionEntrySymbol(fileName: string, position: number, name: string, source: string | undefined): Symbol | undefined; getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined; getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): TextSpan | undefined; getBreakpointStatementAtPosition(fileName: string, position: number): TextSpan | undefined; getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): SignatureHelpItems | undefined; getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): RenameInfo; findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): readonly RenameLocation[] | undefined; getSmartSelectionRange(fileName: string, position: number): SelectionRange; getDefinitionAtPosition(fileName: string, position: number): readonly DefinitionInfo[] | undefined; getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined; getTypeDefinitionAtPosition(fileName: string, position: number): readonly DefinitionInfo[] | undefined; getImplementationAtPosition(fileName: string, position: number): readonly ImplementationLocation[] | undefined; getReferencesAtPosition(fileName: string, position: number): ReferenceEntry[] | undefined; findReferences(fileName: string, position: number): ReferencedSymbol[] | undefined; getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[] | undefined; /** @deprecated */ getOccurrencesAtPosition(fileName: string, position: number): readonly ReferenceEntry[] | undefined; getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string, excludeDtsFiles?: boolean): NavigateToItem[]; getNavigationBarItems(fileName: string): NavigationBarItem[]; getNavigationTree(fileName: string): NavigationTree; getOutliningSpans(fileName: string): OutliningSpan[]; getTodoComments(fileName: string, descriptors: TodoCommentDescriptor[]): TodoComment[]; getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[]; getIndentationAtPosition(fileName: string, position: number, options: EditorOptions | EditorSettings): number; getFormattingEditsForRange(fileName: string, start: number, end: number, options: FormatCodeOptions | FormatCodeSettings): TextChange[];

Slide 6

Slide 6 text

export interface Diagnostic extends DiagnosticRelatedInformation { reportsUnnecessary?: {}; source?: string; relatedInformation?: DiagnosticRelatedInformation[]; } export interface DiagnosticRelatedInformation { category: DiagnosticCategory; code: number; file: SourceFile | undefined; start: number | undefined; length: number | undefined; messageText: string | DiagnosticMessageChain; } interface LanguageService { getSemanticDiagnostics(fileName: string): Diagnostic[]; getSuggestionDiagnostics(fileName: string): DiagnosticWithLocation[]; getCompilerOptionsDiagnostics(): Diagnostic[]; getCompletionsAtPosition(fileName: string, position: number): WithMetadata; getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined; /** more methods... **/ }

Slide 7

Slide 7 text

Position! Position!

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

How to mark this error? const repositoryFragment = gql` fragment RepositoryFragment on Repository { description language(first: 5) { nodes { id } } } `; ~~~~~~~~

Slide 10

Slide 10 text

Flow to find diagnostics 1. Extract text from TS template string node 2. Parse the text to GraphQL AST 3. Find invalid GraphQL node 4. Convert the GraphQL node to TS diagnostics - i.e. Convert GraphQL node position to TS sourceFile position

Slide 11

Slide 11 text

- Enclosing TS template literal node position: 30 - Error location as GraphQL node position range: 67 - 75 - Error location range as TS source: 98 - 106 const repositoryFragment = gql` fragment RepositoryFragment on Repository { description language(first: 5) { nodes { id } } } `; ~~~~~~~~

Slide 12

Slide 12 text

Asserting position problem I want not 98 nor 106 but where the "language" string is. assert.equal(diagnostic.start, 98); assert.equal(diagnostic.end, 106);

Slide 13

Slide 13 text

I made a helper fn - https://github.com/Quramy/ts-graphql-plugin/blob/master/src/graphql-language-service- adapter/get-semantic-diagonistics.test.ts#L69-L84 it('should return position of error token', () => { const frets: Frets = {}; fixture.source = mark( ' const query = `query {`; ' + '\n' + '%%% ^ %%%' + '\n' + '%%% a1 %%%' + '\n', frets, ); const actual = validateFn(); expect(actual[0].start).toBe(frets.a1.pos); });

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

Template expression

Slide 16

Slide 16 text

Template expression - In GraphQL, a template string (query fragment) can be embedded in other template string as expression - I needed to concatenate them statically for GraphQL analysis

Slide 17

Slide 17 text

Composition example /* fragment.ts */ import gql from 'graphql-tag'; export const RepoFragment = gql` fragment Repo on Repository { id name description } `; /* query.ts */ import gql from 'graphql-tag'; import { RepoFragment } from './fragment'; export const query = gql` ${RepoFragment} query MyQuery { viewer { repositories(first: 100) { nodes { ...Repo } } } } `;

Slide 18

Slide 18 text

Flow of static concatenate - If the template literal node is a template expression: - Find ts.Identifier node for each template span - Get where the identifier is declared via languageService.getDefinitionAtPosition for the found identifier - Check whether the declaration is ts.VariableDeclaration ( or other assigning expression / statement) - Extract the next template literal from the RHS - Continue til the template has no interpolation

Slide 19

Slide 19 text

Find template expression /* fragment.ts */ import gql from 'graphql-tag'; export const RepoFragment = gql` fragment Repo on Repository { id name description } `; /* query.ts */ import gql from 'graphql-tag'; import { RepoFragment } from './fragment'; export const query = gql` ${RepoFragment} query MyQuery { viewer { repositories(first: 100) { nodes { ...Repo } } } } `;

Slide 20

Slide 20 text

Find identifiers in template span /* fragment.ts */ import gql from 'graphql-tag'; export const RepoFragment = gql` fragment Repo on Repository { id name description } `; /* query.ts */ import gql from 'graphql-tag'; import { RepoFragment } from './fragment'; export const query = gql` ${RepoFragment} query MyQuery { viewer { repositories(first: 100) { nodes { ...Repo } } } } `;

Slide 21

Slide 21 text

getDefinitionAtPosition /* fragment.ts */ import gql from 'graphql-tag'; export const RepoFragment = gql` fragment Repo on Repository { id name description } `; /* query.ts */ import gql from 'graphql-tag'; import { RepoFragment } from './fragment'; export const query = gql` ${RepoFragment} query MyQuery { viewer { repositories(first: 100) { nodes { ...Repo } } } } `;

Slide 22

Slide 22 text

isVariableDeclaration? /* fragment.ts */ import gql from 'graphql-tag'; export const RepoFragment = gql` fragment Repo on Repository { id name description } `; /* query.ts */ import gql from 'graphql-tag'; import { RepoFragment } from './fragment'; export const query = gql` ${RepoFragment} query MyQuery { viewer { repositories(first: 100) { nodes { ...Repo } } } } `;

Slide 23

Slide 23 text

Extract text from RHS /* fragment.ts */ import gql from 'graphql-tag'; export const RepoFragment = gql` fragment Repo on Repository { id name description } `; /* query.ts */ import gql from 'graphql-tag'; import { RepoFragment } from './fragment'; export const query = gql` ${RepoFragment} query MyQuery { viewer { repositories(first: 100) { nodes { ...Repo } } } } `;

Slide 24

Slide 24 text

The end concatenated result fragment Repo on Repository { id name description } query MyQuery { viewer { repositories(first: 100) { nodes { ...Repo } } } }

Slide 25

Slide 25 text

Summary - To create language service plugin for template string is battle against converting AST location - Language service APIs about code navigations(e.g. go to definition, get references, get call stack) are so much useful in certain situations

Slide 26

Slide 26 text

Thank you !