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 full-size 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 full-size slide

  3. 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 full-size slide

  4. 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 full-size slide

  5. 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 full-size slide

  6. Position! Position!

    View full-size slide

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

    View full-size slide

  8. 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 full-size slide

  9. - 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 full-size slide

  10. 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 full-size slide

  11. 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 full-size slide

  12. Template expression

    View full-size slide

  13. 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 full-size slide

  14. 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 full-size slide

  15. 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 full-size slide

  16. 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 full-size slide

  17. 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 full-size slide

  18. 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 full-size slide

  19. 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 full-size slide

  20. 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 full-size slide

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

    View full-size slide

  22. 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 full-size slide