Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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. Backside of TypeScript
    language service plugin
    #tsc_api_study

    View Slide

  2. 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"
    }
    ]
    }
    }

    View Slide

  3. View Slide

  4. 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

    View Slide

  5. 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[];

    View Slide

  6. 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... **/
    }

    View Slide

  7. Position! Position!

    View Slide

  8. View Slide

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

    View Slide

  10. 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

    View Slide

  11. - 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
    }
    }
    }
    `;
    ~~~~~~~~

    View Slide

  12. 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);

    View Slide

  13. 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);
    });

    View Slide

  14. View Slide

  15. Template expression

    View Slide

  16. 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

    View Slide

  17. 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
    }
    }
    }
    }
    `;

    View Slide

  18. 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

    View Slide

  19. 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
    }
    }
    }
    }
    `;

    View Slide

  20. 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
    }
    }
    }
    }
    `;

    View Slide

  21. 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
    }
    }
    }
    }
    `;

    View Slide

  22. 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
    }
    }
    }
    }
    `;

    View Slide

  23. 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
    }
    }
    }
    }
    `;

    View Slide

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

    View Slide

  25. 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

    View Slide

  26. Thank you !

    View Slide