$30 off During Our Annual Pro Sale. View Details »

TypeScript Language Service Plugin backside

TypeScript Language Service Plugin backside

Yosuke Kurami

March 04, 2020
Tweet

More Decks by Yosuke Kurami

Other Decks in Programming

Transcript

  1. 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" } ] } }
  2. 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
  3. 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<CompletionInfo> | 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[];
  4. 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<CompletionInfo>; getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined; /** more methods... **/ }
  5. How to mark this error? const repositoryFragment = gql` fragment

    RepositoryFragment on Repository { description language(first: 5) { nodes { id } } } `; ~~~~~~~~
  6. 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
  7. - 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 } } } `; ~~~~~~~~
  8. 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);
  9. 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); });
  10. 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
  11. 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 } } } } `;
  12. 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
  13. 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 } } } } `;
  14. 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 } } } } `;
  15. 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 } } } } `;
  16. 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 } } } } `;
  17. 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 } } } } `;
  18. The end concatenated result fragment Repo on Repository { id

    name description } query MyQuery { viewer { repositories(first: 100) { nodes { ...Repo } } } }
  19. 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