Mirror: TypeScript LSP plugin that finds GraphQL documents in your code and provides diagnostics, auto-complete and hover-information.

feat(lsp): support prop assignment (#202)

Changed files
+311 -283
.changeset
packages
example-tada
graphqlsp
src
ast
+5
.changeset/angry-eyes-occur.md
···
···
+
---
+
'@0no-co/graphqlsp': minor
+
---
+
+
support property assignment/objectAccessPattern
+282 -279
packages/example-tada/introspection.ts
···
/** An IntrospectionQuery representation of your schema.
*
* @remarks
···
* ```
*/
const introspection = {
-
__schema: {
-
queryType: {
-
name: 'Query',
},
-
mutationType: null,
-
subscriptionType: null,
-
types: [
{
-
kind: 'OBJECT',
-
name: 'Attack',
-
fields: [
{
-
name: 'damage',
-
type: {
-
kind: 'SCALAR',
-
name: 'Int',
-
ofType: null,
},
-
args: [],
},
{
-
name: 'name',
-
type: {
-
kind: 'SCALAR',
-
name: 'String',
-
ofType: null,
},
-
args: [],
},
{
-
name: 'type',
-
type: {
-
kind: 'ENUM',
-
name: 'PokemonType',
-
ofType: null,
},
-
args: [],
-
},
],
-
interfaces: [],
},
{
-
kind: 'SCALAR',
-
name: 'Int',
},
{
-
kind: 'SCALAR',
-
name: 'String',
},
{
-
kind: 'OBJECT',
-
name: 'AttacksConnection',
-
fields: [
{
-
name: 'fast',
-
type: {
-
kind: 'LIST',
-
ofType: {
-
kind: 'OBJECT',
-
name: 'Attack',
-
ofType: null,
-
},
},
-
args: [],
},
{
-
name: 'special',
-
type: {
-
kind: 'LIST',
-
ofType: {
-
kind: 'OBJECT',
-
name: 'Attack',
-
ofType: null,
-
},
},
-
args: [],
-
},
],
-
interfaces: [],
},
{
-
kind: 'OBJECT',
-
name: 'EvolutionRequirement',
-
fields: [
{
-
name: 'amount',
-
type: {
-
kind: 'SCALAR',
-
name: 'Int',
-
ofType: null,
},
-
args: [],
},
{
-
name: 'name',
-
type: {
-
kind: 'SCALAR',
-
name: 'String',
-
ofType: null,
},
-
args: [],
-
},
],
-
interfaces: [],
},
{
-
kind: 'OBJECT',
-
name: 'Pokemon',
-
fields: [
{
-
name: 'attacks',
-
type: {
-
kind: 'OBJECT',
-
name: 'AttacksConnection',
-
ofType: null,
},
-
args: [],
},
{
-
name: 'classification',
-
type: {
-
kind: 'SCALAR',
-
name: 'String',
-
ofType: null,
},
-
args: [],
},
{
-
name: 'evolutionRequirements',
-
type: {
-
kind: 'LIST',
-
ofType: {
-
kind: 'OBJECT',
-
name: 'EvolutionRequirement',
-
ofType: null,
-
},
},
-
args: [],
},
{
-
name: 'evolutions',
-
type: {
-
kind: 'LIST',
-
ofType: {
-
kind: 'OBJECT',
-
name: 'Pokemon',
-
ofType: null,
-
},
},
-
args: [],
},
{
-
name: 'fleeRate',
-
type: {
-
kind: 'SCALAR',
-
name: 'Float',
-
ofType: null,
},
-
args: [],
},
{
-
name: 'height',
-
type: {
-
kind: 'OBJECT',
-
name: 'PokemonDimension',
-
ofType: null,
},
-
args: [],
},
{
-
name: 'id',
-
type: {
-
kind: 'NON_NULL',
-
ofType: {
-
kind: 'SCALAR',
-
name: 'ID',
-
ofType: null,
-
},
},
-
args: [],
},
{
-
name: 'maxCP',
-
type: {
-
kind: 'SCALAR',
-
name: 'Int',
-
ofType: null,
},
-
args: [],
},
{
-
name: 'maxHP',
-
type: {
-
kind: 'SCALAR',
-
name: 'Int',
-
ofType: null,
},
-
args: [],
},
{
-
name: 'name',
-
type: {
-
kind: 'NON_NULL',
-
ofType: {
-
kind: 'SCALAR',
-
name: 'String',
-
ofType: null,
-
},
},
-
args: [],
},
{
-
name: 'resistant',
-
type: {
-
kind: 'LIST',
-
ofType: {
-
kind: 'ENUM',
-
name: 'PokemonType',
-
ofType: null,
-
},
},
-
args: [],
},
{
-
name: 'types',
-
type: {
-
kind: 'LIST',
-
ofType: {
-
kind: 'ENUM',
-
name: 'PokemonType',
-
ofType: null,
-
},
},
-
args: [],
},
{
-
name: 'weaknesses',
-
type: {
-
kind: 'LIST',
-
ofType: {
-
kind: 'ENUM',
-
name: 'PokemonType',
-
ofType: null,
-
},
},
-
args: [],
},
{
-
name: 'weight',
-
type: {
-
kind: 'OBJECT',
-
name: 'PokemonDimension',
-
ofType: null,
},
-
args: [],
-
},
],
-
interfaces: [],
},
{
-
kind: 'SCALAR',
-
name: 'Float',
},
{
-
kind: 'SCALAR',
-
name: 'ID',
},
{
-
kind: 'OBJECT',
-
name: 'PokemonDimension',
-
fields: [
{
-
name: 'maximum',
-
type: {
-
kind: 'SCALAR',
-
name: 'String',
-
ofType: null,
},
-
args: [],
},
{
-
name: 'minimum',
-
type: {
-
kind: 'SCALAR',
-
name: 'String',
-
ofType: null,
},
-
args: [],
-
},
],
-
interfaces: [],
},
{
-
kind: 'ENUM',
-
name: 'PokemonType',
-
enumValues: [
{
-
name: 'Bug',
},
{
-
name: 'Dark',
},
{
-
name: 'Dragon',
},
{
-
name: 'Electric',
},
{
-
name: 'Fairy',
},
{
-
name: 'Fighting',
},
{
-
name: 'Fire',
},
{
-
name: 'Flying',
},
{
-
name: 'Ghost',
},
{
-
name: 'Grass',
},
{
-
name: 'Ground',
},
{
-
name: 'Ice',
},
{
-
name: 'Normal',
},
{
-
name: 'Poison',
},
{
-
name: 'Psychic',
},
{
-
name: 'Rock',
},
{
-
name: 'Steel',
},
{
-
name: 'Water',
-
},
-
],
},
{
-
kind: 'OBJECT',
-
name: 'Query',
-
fields: [
{
-
name: 'pokemon',
-
type: {
-
kind: 'OBJECT',
-
name: 'Pokemon',
-
ofType: null,
},
-
args: [
{
-
name: 'id',
-
type: {
-
kind: 'NON_NULL',
-
ofType: {
-
kind: 'SCALAR',
-
name: 'ID',
-
ofType: null,
-
},
-
},
-
},
-
],
},
{
-
name: 'pokemons',
-
type: {
-
kind: 'LIST',
-
ofType: {
-
kind: 'OBJECT',
-
name: 'Pokemon',
-
ofType: null,
-
},
},
-
args: [
{
-
name: 'limit',
-
type: {
-
kind: 'SCALAR',
-
name: 'Int',
-
ofType: null,
-
},
},
{
-
name: 'skip',
-
type: {
-
kind: 'SCALAR',
-
name: 'Int',
-
ofType: null,
-
},
-
},
-
],
-
},
],
-
interfaces: [],
},
{
-
kind: 'SCALAR',
-
name: 'Boolean',
},
{
-
kind: 'SCALAR',
-
name: 'Any',
-
},
],
-
directives: [],
-
},
} as const;
export { introspection };
···
+
/* eslint-disable */
+
/* prettier-ignore */
+
/** An IntrospectionQuery representation of your schema.
*
* @remarks
···
* ```
*/
const introspection = {
+
"__schema": {
+
"queryType": {
+
"name": "Query"
},
+
"mutationType": null,
+
"subscriptionType": null,
+
"types": [
{
+
"kind": "OBJECT",
+
"name": "Attack",
+
"fields": [
{
+
"name": "damage",
+
"type": {
+
"kind": "SCALAR",
+
"name": "Int",
+
"ofType": null
},
+
"args": []
},
{
+
"name": "name",
+
"type": {
+
"kind": "SCALAR",
+
"name": "String",
+
"ofType": null
},
+
"args": []
},
{
+
"name": "type",
+
"type": {
+
"kind": "ENUM",
+
"name": "PokemonType",
+
"ofType": null
},
+
"args": []
+
}
],
+
"interfaces": []
},
{
+
"kind": "SCALAR",
+
"name": "Int"
},
{
+
"kind": "SCALAR",
+
"name": "String"
},
{
+
"kind": "OBJECT",
+
"name": "AttacksConnection",
+
"fields": [
{
+
"name": "fast",
+
"type": {
+
"kind": "LIST",
+
"ofType": {
+
"kind": "OBJECT",
+
"name": "Attack",
+
"ofType": null
+
}
},
+
"args": []
},
{
+
"name": "special",
+
"type": {
+
"kind": "LIST",
+
"ofType": {
+
"kind": "OBJECT",
+
"name": "Attack",
+
"ofType": null
+
}
},
+
"args": []
+
}
],
+
"interfaces": []
},
{
+
"kind": "OBJECT",
+
"name": "EvolutionRequirement",
+
"fields": [
{
+
"name": "amount",
+
"type": {
+
"kind": "SCALAR",
+
"name": "Int",
+
"ofType": null
},
+
"args": []
},
{
+
"name": "name",
+
"type": {
+
"kind": "SCALAR",
+
"name": "String",
+
"ofType": null
},
+
"args": []
+
}
],
+
"interfaces": []
},
{
+
"kind": "OBJECT",
+
"name": "Pokemon",
+
"fields": [
{
+
"name": "attacks",
+
"type": {
+
"kind": "OBJECT",
+
"name": "AttacksConnection",
+
"ofType": null
},
+
"args": []
},
{
+
"name": "classification",
+
"type": {
+
"kind": "SCALAR",
+
"name": "String",
+
"ofType": null
},
+
"args": []
},
{
+
"name": "evolutionRequirements",
+
"type": {
+
"kind": "LIST",
+
"ofType": {
+
"kind": "OBJECT",
+
"name": "EvolutionRequirement",
+
"ofType": null
+
}
},
+
"args": []
},
{
+
"name": "evolutions",
+
"type": {
+
"kind": "LIST",
+
"ofType": {
+
"kind": "OBJECT",
+
"name": "Pokemon",
+
"ofType": null
+
}
},
+
"args": []
},
{
+
"name": "fleeRate",
+
"type": {
+
"kind": "SCALAR",
+
"name": "Float",
+
"ofType": null
},
+
"args": []
},
{
+
"name": "height",
+
"type": {
+
"kind": "OBJECT",
+
"name": "PokemonDimension",
+
"ofType": null
},
+
"args": []
},
{
+
"name": "id",
+
"type": {
+
"kind": "NON_NULL",
+
"ofType": {
+
"kind": "SCALAR",
+
"name": "ID",
+
"ofType": null
+
}
},
+
"args": []
},
{
+
"name": "maxCP",
+
"type": {
+
"kind": "SCALAR",
+
"name": "Int",
+
"ofType": null
},
+
"args": []
},
{
+
"name": "maxHP",
+
"type": {
+
"kind": "SCALAR",
+
"name": "Int",
+
"ofType": null
},
+
"args": []
},
{
+
"name": "name",
+
"type": {
+
"kind": "NON_NULL",
+
"ofType": {
+
"kind": "SCALAR",
+
"name": "String",
+
"ofType": null
+
}
},
+
"args": []
},
{
+
"name": "resistant",
+
"type": {
+
"kind": "LIST",
+
"ofType": {
+
"kind": "ENUM",
+
"name": "PokemonType",
+
"ofType": null
+
}
},
+
"args": []
},
{
+
"name": "types",
+
"type": {
+
"kind": "LIST",
+
"ofType": {
+
"kind": "ENUM",
+
"name": "PokemonType",
+
"ofType": null
+
}
},
+
"args": []
},
{
+
"name": "weaknesses",
+
"type": {
+
"kind": "LIST",
+
"ofType": {
+
"kind": "ENUM",
+
"name": "PokemonType",
+
"ofType": null
+
}
},
+
"args": []
},
{
+
"name": "weight",
+
"type": {
+
"kind": "OBJECT",
+
"name": "PokemonDimension",
+
"ofType": null
},
+
"args": []
+
}
],
+
"interfaces": []
},
{
+
"kind": "SCALAR",
+
"name": "Float"
},
{
+
"kind": "SCALAR",
+
"name": "ID"
},
{
+
"kind": "OBJECT",
+
"name": "PokemonDimension",
+
"fields": [
{
+
"name": "maximum",
+
"type": {
+
"kind": "SCALAR",
+
"name": "String",
+
"ofType": null
},
+
"args": []
},
{
+
"name": "minimum",
+
"type": {
+
"kind": "SCALAR",
+
"name": "String",
+
"ofType": null
},
+
"args": []
+
}
],
+
"interfaces": []
},
{
+
"kind": "ENUM",
+
"name": "PokemonType",
+
"enumValues": [
{
+
"name": "Bug"
},
{
+
"name": "Dark"
},
{
+
"name": "Dragon"
},
{
+
"name": "Electric"
},
{
+
"name": "Fairy"
},
{
+
"name": "Fighting"
},
{
+
"name": "Fire"
},
{
+
"name": "Flying"
},
{
+
"name": "Ghost"
},
{
+
"name": "Grass"
},
{
+
"name": "Ground"
},
{
+
"name": "Ice"
},
{
+
"name": "Normal"
},
{
+
"name": "Poison"
},
{
+
"name": "Psychic"
},
{
+
"name": "Rock"
},
{
+
"name": "Steel"
},
{
+
"name": "Water"
+
}
+
]
},
{
+
"kind": "OBJECT",
+
"name": "Query",
+
"fields": [
{
+
"name": "pokemon",
+
"type": {
+
"kind": "OBJECT",
+
"name": "Pokemon",
+
"ofType": null
},
+
"args": [
{
+
"name": "id",
+
"type": {
+
"kind": "NON_NULL",
+
"ofType": {
+
"kind": "SCALAR",
+
"name": "ID",
+
"ofType": null
+
}
+
}
+
}
+
]
},
{
+
"name": "pokemons",
+
"type": {
+
"kind": "LIST",
+
"ofType": {
+
"kind": "OBJECT",
+
"name": "Pokemon",
+
"ofType": null
+
}
},
+
"args": [
{
+
"name": "limit",
+
"type": {
+
"kind": "SCALAR",
+
"name": "Int",
+
"ofType": null
+
}
},
{
+
"name": "skip",
+
"type": {
+
"kind": "SCALAR",
+
"name": "Int",
+
"ofType": null
+
}
+
}
+
]
+
}
],
+
"interfaces": []
},
{
+
"kind": "SCALAR",
+
"name": "Boolean"
},
{
+
"kind": "SCALAR",
+
"name": "Any"
+
}
],
+
"directives": []
+
}
} as const;
export { introspection };
+10 -2
packages/example-tada/src/Pokemon.tsx
···
import { FragmentOf, graphql, readFragment } from './graphql';
export const PokemonFields = graphql(`
fragment pokemonFields on Pokemon {
name
···
`);
interface Props {
-
data: FragmentOf<typeof PokemonFields> | null;
}
export const Pokemon = ({ data }: Props) => {
const pokemon = readFragment(PokemonFields, data);
-
if (!pokemon) {
return null;
}
return (
<li>
{pokemon.name}
</li>
);
};
···
import { FragmentOf, graphql, readFragment } from './graphql';
+
export const Fields = { Pokemon: graphql(`
+
fragment Pok on Pokemon {
+
resistant
+
}`)
+
}
+
export const PokemonFields = graphql(`
fragment pokemonFields on Pokemon {
name
···
`);
interface Props {
+
data: (FragmentOf<typeof PokemonFields> & FragmentOf<typeof Fields.Pokemon>) | null;
}
export const Pokemon = ({ data }: Props) => {
const pokemon = readFragment(PokemonFields, data);
+
const resistant = readFragment(Fields.Pokemon, data);
+
if (!pokemon || !resistant) {
return null;
}
return (
<li>
{pokemon.name}
+
{resistant.resistant}
</li>
);
};
+3 -2
packages/example-tada/src/index.tsx
···
import { createClient, useQuery } from 'urql';
import { graphql } from './graphql';
-
import { Pokemon, PokemonFields } from './Pokemon';
const PokemonQuery = graphql(`
query Po($id: ID!) {
···
id
fleeRate
...pokemonFields
attacks {
special {
name
···
types
}
}
-
`, [PokemonFields]);
const Pokemons = () => {
const [result] = useQuery({
···
import { createClient, useQuery } from 'urql';
import { graphql } from './graphql';
+
import { Fields, Pokemon, PokemonFields } from './Pokemon';
const PokemonQuery = graphql(`
query Po($id: ID!) {
···
id
fleeRate
...pokemonFields
+
...Pok
attacks {
special {
name
···
types
}
}
+
`, [PokemonFields, Fields.Pokemon]);
const Pokemons = () => {
const [result] = useQuery({
+11
packages/graphqlsp/src/ast/index.ts
···
ts.isCallExpression(found.parent.initializer)
) {
found = found.parent.initializer;
}
if (ts.isCallExpression(found) && templates.has(found.expression.getText())) {
···
arg2.elements.forEach(element => {
if (ts.isIdentifier(element)) {
fragments.push(...unrollFragment(element, info));
}
});
}
···
ts.isCallExpression(found.parent.initializer)
) {
found = found.parent.initializer;
+
} else if (ts.isPropertyAssignment(found.parent)) {
+
found = found.parent.initializer;
}
if (ts.isCallExpression(found) && templates.has(found.expression.getText())) {
···
arg2.elements.forEach(element => {
if (ts.isIdentifier(element)) {
fragments.push(...unrollFragment(element, info));
+
} else if (ts.isPropertyAccessExpression(element)) {
+
let el = element;
+
while (ts.isPropertyAccessExpression(el.expression)) {
+
el = el.expression;
+
}
+
+
if (ts.isIdentifier(el.name)) {
+
fragments.push(...unrollFragment(el.name, info));
+
}
}
});
}