diff --git a/resources/benchmark.ts b/resources/benchmark.ts index 2d7216c2b1..d0fefa26a5 100644 --- a/resources/benchmark.ts +++ b/resources/benchmark.ts @@ -270,9 +270,9 @@ function runBenchmark( benchmark: string, benchmarkProjects: ReadonlyArray, ) { - const results = []; - for (let i = 0; i < benchmarkProjects.length; ++i) { - const { revision, projectPath } = benchmarkProjects[i]; + const results: Array = []; + + benchmarkProjects.forEach(({ revision, projectPath }, i) => { const modulePath = path.join(projectPath, benchmark); if (i === 0) { @@ -288,7 +288,8 @@ function runBenchmark( } catch (error) { console.log(' ' + revision + ': ' + red(error.message)); } - } + }); + console.log('\n'); beautifyBenchmark(results); diff --git a/resources/build-npm.ts b/resources/build-npm.ts index 4ccd249a76..a5aecf48d8 100644 --- a/resources/build-npm.ts +++ b/resources/build-npm.ts @@ -69,6 +69,7 @@ function buildPackage(outDir: string, isESMOnly: boolean): void { const splittedTag = preReleaseTag.split('.'); // Note: `experimental-*` take precedence over `alpha`, `beta` or `rc`. const versionTag = splittedTag[2] ?? splittedTag[0]; + assert(versionTag); assert( ['alpha', 'beta', 'rc'].includes(versionTag) || versionTag.startsWith('experimental-'), diff --git a/resources/diff-npm-package.ts b/resources/diff-npm-package.ts index 147e4874fe..dbf6dee25d 100644 --- a/resources/diff-npm-package.ts +++ b/resources/diff-npm-package.ts @@ -24,6 +24,9 @@ if (args.length < 2) { ); } +assert(fromRevision); +assert(toRevision); + console.log(`📦 Building NPM package for ${fromRevision}...`); const fromPackage = prepareNPMPackage(fromRevision); diff --git a/resources/gen-changelog.ts b/resources/gen-changelog.ts index 9d106fbacc..7e30bbbbed 100644 --- a/resources/gen-changelog.ts +++ b/resources/gen-changelog.ts @@ -1,3 +1,5 @@ +import assert from 'node:assert'; + import { git, readPackageJSON } from './utils.js'; const packageJSON = readPackageJSON(); @@ -88,11 +90,13 @@ async function genChangeLog(): Promise { } const label = labels[0]; + assert(label); if (!labelsConfig[label]) { throw new Error(`Unknown label: ${label}. See ${pr.url}`); } - byLabel[label] ??= []; - byLabel[label].push(pr); + const prByLabel = byLabel[label] ?? []; + byLabel[label] = prByLabel; + prByLabel.push(pr); committersByLogin[pr.author.login] = pr.author; } @@ -285,7 +289,11 @@ function commitInfoToPR(commit: CommitInfo): number { ); } - return associatedPRs[0].number; + const pr = associatedPRs[0]; + + assert(pr); + + return pr.number; } async function getPRsInfo( diff --git a/resources/inline-invariant.ts b/resources/inline-invariant.ts index ed5d8e5739..e11794daa6 100644 --- a/resources/inline-invariant.ts +++ b/resources/inline-invariant.ts @@ -1,3 +1,5 @@ +import assert from 'node:assert'; + import ts from 'typescript'; /** @@ -29,6 +31,8 @@ export function inlineInvariant(context: ts.TransformationContext) { if (funcName === 'invariant' || funcName === 'devAssert') { const [condition, ...otherArgs] = args; + assert(condition); + return factory.createBinaryExpression( factory.createParenthesizedExpression(condition), ts.SyntaxKind.BarBarToken, diff --git a/resources/utils.ts b/resources/utils.ts index ed46f9635a..fa11571d1d 100644 --- a/resources/utils.ts +++ b/resources/utils.ts @@ -149,11 +149,12 @@ export function showDirStats(dirPath: string): void { const ext = name.split('.').slice(1).join('.'); const filetype = ext ? '*.' + ext : name; - fileTypes[filetype] ??= { filepaths: [], size: 0 }; + const dirStats = fileTypes[filetype] ?? { filepaths: [], size: 0 }; + fileTypes[filetype] = dirStats; totalSize += stats.size; - fileTypes[filetype].size += stats.size; - fileTypes[filetype].filepaths.push(filepath); + dirStats.size += stats.size; + dirStats.filepaths.push(filepath); } const stats: Array<[string, number]> = []; @@ -163,13 +164,14 @@ export function showDirStats(dirPath: string): void { if (numFiles > 1) { stats.push([filetype + ' x' + numFiles, typeStats.size]); } else { + assert(typeStats.filepaths[0]); const relativePath = path.relative(dirPath, typeStats.filepaths[0]); stats.push([relativePath, typeStats.size]); } } stats.sort((a, b) => b[1] - a[1]); - const prettyStats = stats.map(([type, size]) => [ + const prettyStats = stats.map<[string, string]>(([type, size]) => [ type, (size / 1024).toFixed(2) + ' KB', ]); diff --git a/src/__testUtils__/dedent.ts b/src/__testUtils__/dedent.ts index 2085286165..8ec4868e36 100644 --- a/src/__testUtils__/dedent.ts +++ b/src/__testUtils__/dedent.ts @@ -32,10 +32,10 @@ export function dedent( strings: ReadonlyArray, ...values: ReadonlyArray ): string { - let str = strings[0]; + let str = `${strings[0]}`; for (let i = 1; i < strings.length; ++i) { - str += values[i - 1] + strings[i]; // interpolation + str += `${values[i - 1]}${strings[i]}`; // interpolation } return dedentString(str); } diff --git a/src/__testUtils__/expectMatchingValues.ts b/src/__testUtils__/expectMatchingValues.ts index 7285a9b9df..4331f56bed 100644 --- a/src/__testUtils__/expectMatchingValues.ts +++ b/src/__testUtils__/expectMatchingValues.ts @@ -2,8 +2,16 @@ import { expectJSON } from './expectJSON.js'; export function expectMatchingValues(values: ReadonlyArray): T { const [firstValue, ...remainingValues] = values; + + /* c8 ignore start */ + if (firstValue === undefined) { + throw new Error('Expected a non-empty array'); + } + /* c8 ignore stop */ + for (const value of remainingValues) { expectJSON(value).toDeepEqual(firstValue); } + return firstValue; } diff --git a/src/__tests__/starWarsData.ts b/src/__tests__/starWarsData.ts index 60c4331bb6..649613698a 100644 --- a/src/__tests__/starWarsData.ts +++ b/src/__tests__/starWarsData.ts @@ -9,7 +9,7 @@ export interface Character { appearsIn: ReadonlyArray; } -export interface Human { +export interface Human extends Character { type: 'Human'; id: string; name: string; @@ -18,7 +18,7 @@ export interface Human { homePlanet?: string; } -export interface Droid { +export interface Droid extends Character { type: 'Droid'; id: string; name: string; @@ -35,93 +35,110 @@ export interface Droid { * JSON objects in a more complex demo. */ -const luke: Human = { +const luke = { type: 'Human', id: '1000', name: 'Luke Skywalker', friends: ['1002', '1003', '2000', '2001'], appearsIn: [4, 5, 6], homePlanet: 'Tatooine', -}; +} as const satisfies Human; -const vader: Human = { +const vader = { type: 'Human', id: '1001', name: 'Darth Vader', friends: ['1004'], appearsIn: [4, 5, 6], homePlanet: 'Tatooine', -}; +} as const satisfies Human; -const han: Human = { +const han = { type: 'Human', id: '1002', name: 'Han Solo', friends: ['1000', '1003', '2001'], appearsIn: [4, 5, 6], -}; +} as const satisfies Human; -const leia: Human = { +const leia = { type: 'Human', id: '1003', name: 'Leia Organa', friends: ['1000', '1002', '2000', '2001'], appearsIn: [4, 5, 6], homePlanet: 'Alderaan', -}; +} as const satisfies Human; -const tarkin: Human = { +const tarkin = { type: 'Human', id: '1004', name: 'Wilhuff Tarkin', friends: ['1001'], appearsIn: [4], -}; +} as const satisfies Human; -const humanData: { [id: string]: Human } = { +const humanData = { [luke.id]: luke, [vader.id]: vader, [han.id]: han, [leia.id]: leia, [tarkin.id]: tarkin, -}; +} as const satisfies { [id: string]: Human }; -const threepio: Droid = { +type HumanData = typeof humanData; +type AnyHumanId = keyof HumanData; +type AnyHuman = HumanData[AnyHumanId]; + +const threepio = { type: 'Droid', id: '2000', name: 'C-3PO', friends: ['1000', '1002', '1003', '2001'], appearsIn: [4, 5, 6], primaryFunction: 'Protocol', -}; +} as const satisfies Droid; -const artoo: Droid = { +const artoo = { type: 'Droid', id: '2001', name: 'R2-D2', friends: ['1000', '1002', '1003'], appearsIn: [4, 5, 6], primaryFunction: 'Astromech', -}; +} as const satisfies Droid; -const droidData: { [id: string]: Droid } = { +const droidData = { [threepio.id]: threepio, [artoo.id]: artoo, -}; +} as const satisfies { [id: string]: Droid }; + +type DroidData = typeof droidData; +type AnyDroidId = keyof DroidData; +type AnyDroid = DroidData[AnyDroidId]; + +type AnyCharacter = AnyHuman | AnyDroid; +type AnyCharacterId = AnyCharacter['id']; + +const isHumanId = (id: AnyCharacterId): id is AnyHumanId => + Object.hasOwn(humanData, id); /** * Helper function to get a character by ID. */ -function getCharacter(id: string): Promise { - // Returning a promise just to illustrate that GraphQL.js supports it. - return Promise.resolve(humanData[id] ?? droidData[id]); +function getCharacter(id: AnyCharacterId): Promise { + if (isHumanId(id)) { + return Promise.resolve(humanData[id]); + } + + return Promise.resolve(droidData[id]); } /** * Allows us to query for a character's friends. */ -export function getFriends( - character: Character, +export function getFriends( + character: T, ): Array> { // Notice that GraphQL accepts Arrays of Promises. return character.friends.map((id) => getCharacter(id)); @@ -142,13 +159,13 @@ export function getHero(episode: number): Character { /** * Allows us to query for the human with the given id. */ -export function getHuman(id: string): Human | null { +export function getHuman(id: AnyHumanId): AnyHuman { return humanData[id]; } /** * Allows us to query for the droid with the given id. */ -export function getDroid(id: string): Droid | null { +export function getDroid(id: AnyDroidId): Droid { return droidData[id]; } diff --git a/src/error/__tests__/GraphQLError-test.ts b/src/error/__tests__/GraphQLError-test.ts index 7cfda2a5a3..64eedcf7f3 100644 --- a/src/error/__tests__/GraphQLError-test.ts +++ b/src/error/__tests__/GraphQLError-test.ts @@ -16,7 +16,7 @@ const source = new Source(dedent` `); const ast = parse(source); const operationNode = ast.definitions[0]; -assert(operationNode.kind === Kind.OPERATION_DEFINITION); +assert(operationNode?.kind === Kind.OPERATION_DEFINITION); const fieldNode = operationNode.selectionSet.selections[0]; assert(fieldNode != null); @@ -247,8 +247,9 @@ describe('toString', () => { ), ); const opA = docA.definitions[0]; - assert(opA.kind === Kind.OBJECT_TYPE_DEFINITION && opA.fields != null); + assert(opA?.kind === Kind.OBJECT_TYPE_DEFINITION && opA.fields != null); const fieldA = opA.fields[0]; + assert(fieldA); const docB = parse( new Source( @@ -261,8 +262,9 @@ describe('toString', () => { ), ); const opB = docB.definitions[0]; - assert(opB.kind === Kind.OBJECT_TYPE_DEFINITION && opB.fields != null); + assert(opB?.kind === Kind.OBJECT_TYPE_DEFINITION && opB.fields != null); const fieldB = opB.fields[0]; + assert(fieldB); const error = new GraphQLError('Example error with two nodes', { nodes: [fieldA.type, fieldB.type], diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 5e25dddb5f..fb3f006a8d 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -225,7 +225,7 @@ describe('Execute: Handles basic execution tasks', () => { ); const operation = document.definitions[0]; - assert(operation.kind === Kind.OPERATION_DEFINITION); + assert(operation?.kind === Kind.OPERATION_DEFINITION); expect(resolvedInfo).to.include({ fieldName: 'test', diff --git a/src/execution/__tests__/mapAsyncIterable-test.ts b/src/execution/__tests__/mapAsyncIterable-test.ts index 0b26ab7327..f28c6a501e 100644 --- a/src/execution/__tests__/mapAsyncIterable-test.ts +++ b/src/execution/__tests__/mapAsyncIterable-test.ts @@ -7,6 +7,16 @@ import { mapAsyncIterable } from '../mapAsyncIterable.js'; /* eslint-disable @typescript-eslint/require-await */ describe('mapAsyncIterable', () => { + const doubler = (x: number | undefined) => { + /* c8 ignore start */ + if (x === undefined) { + throw new Error('Unexpected undefined in iterator'); + } + /* c8 ignore stop */ + + return x + x; + }; + it('maps over async generator', async () => { async function* source() { yield 1; @@ -14,7 +24,7 @@ describe('mapAsyncIterable', () => { yield 3; } - const doubles = mapAsyncIterable(source(), (x) => x + x); + const doubles = mapAsyncIterable(source(), doubler); expect(await doubles.next()).to.deep.equal({ value: 2, done: false }); expect(await doubles.next()).to.deep.equal({ value: 4, done: false }); @@ -34,9 +44,9 @@ describe('mapAsyncIterable', () => { }, next(): Promise> { - if (items.length > 0) { - const value = items[0]; - items.shift(); + const value = items.shift(); + + if (value) { return Promise.resolve({ done: false, value }); } @@ -44,7 +54,7 @@ describe('mapAsyncIterable', () => { }, }; - const doubles = mapAsyncIterable(iterable, (x) => x + x); + const doubles = mapAsyncIterable(iterable, doubler); expect(await doubles.next()).to.deep.equal({ value: 2, done: false }); expect(await doubles.next()).to.deep.equal({ value: 4, done: false }); @@ -62,7 +72,7 @@ describe('mapAsyncIterable', () => { yield 3; } - const doubles = mapAsyncIterable(source(), (x) => x + x); + const doubles = mapAsyncIterable(source(), doubler); const result = []; for await (const x of doubles) { @@ -102,7 +112,7 @@ describe('mapAsyncIterable', () => { } } - const doubles = mapAsyncIterable(source(), (x) => x + x); + const doubles = mapAsyncIterable(source(), doubler); expect(await doubles.next()).to.deep.equal({ value: 2, done: false }); expect(await doubles.next()).to.deep.equal({ value: 4, done: false }); @@ -141,7 +151,7 @@ describe('mapAsyncIterable', () => { }, }; - const doubles = mapAsyncIterable(iterable, (x) => x + x); + const doubles = mapAsyncIterable(iterable, doubler); expect(await doubles.next()).to.deep.equal({ value: 2, done: false }); expect(await doubles.next()).to.deep.equal({ value: 4, done: false }); @@ -205,7 +215,7 @@ describe('mapAsyncIterable', () => { }, }; - const doubles = mapAsyncIterable(iterable, (x) => x + x); + const doubles = mapAsyncIterable(iterable, doubler); expect(await doubles.next()).to.deep.equal({ value: 2, done: false }); expect(await doubles.next()).to.deep.equal({ value: 4, done: false }); @@ -228,7 +238,7 @@ describe('mapAsyncIterable', () => { } } - const doubles = mapAsyncIterable(source(), (x) => x + x); + const doubles = mapAsyncIterable(source(), doubler); expect(await doubles.next()).to.deep.equal({ value: 2, done: false }); expect(await doubles.next()).to.deep.equal({ value: 4, done: false }); diff --git a/src/execution/__tests__/simplePubSub.ts b/src/execution/__tests__/simplePubSub.ts index f535ac454b..fabbe0c489 100644 --- a/src/execution/__tests__/simplePubSub.ts +++ b/src/execution/__tests__/simplePubSub.ts @@ -40,11 +40,11 @@ export class SimplePubSub { return Promise.resolve({ value: undefined, done: true }); } - if (pushQueue.length > 0) { - const value = pushQueue[0]; - pushQueue.shift(); + const value = pushQueue.shift(); + if (value !== undefined) { return Promise.resolve({ value, done: false }); } + return new Promise((resolve) => pullQueue.push(resolve)); }, return(): Promise> { diff --git a/src/execution/__tests__/stream-test.ts b/src/execution/__tests__/stream-test.ts index cd9b9b3965..c8f1a37239 100644 --- a/src/execution/__tests__/stream-test.ts +++ b/src/execution/__tests__/stream-test.ts @@ -871,7 +871,7 @@ describe('Execute: stream directive', () => { } `); const result = await complete(document, { - scalarList: () => [friends[0].name, {}], + scalarList: () => [friends[0]?.name, {}], }); expectJSON(result).toDeepEqual([ { @@ -908,11 +908,11 @@ describe('Execute: stream directive', () => { `); const result = await complete(document, { friendList: () => [ - Promise.resolve({ nonNullName: friends[0].name }), + Promise.resolve({ nonNullName: friends[0]?.name }), Promise.resolve({ nonNullName: () => Promise.reject(new Error('Oops')), }), - Promise.resolve({ nonNullName: friends[1].name }), + Promise.resolve({ nonNullName: friends[1]?.name }), ], }); expectJSON(result).toDeepEqual([ @@ -959,9 +959,9 @@ describe('Execute: stream directive', () => { `); const result = await complete(document, { friendList: () => [ - { nonNullName: Promise.resolve(friends[0].name) }, + { nonNullName: Promise.resolve(friends[0]?.name) }, { nonNullName: Promise.reject(new Error('Oops')) }, - { nonNullName: Promise.resolve(friends[1].name) }, + { nonNullName: Promise.resolve(friends[1]?.name) }, ], }); expectJSON(result).toDeepEqual([ @@ -1008,11 +1008,11 @@ describe('Execute: stream directive', () => { `); const result = await complete(document, { nonNullFriendList: () => [ - Promise.resolve({ nonNullName: friends[0].name }), + Promise.resolve({ nonNullName: friends[0]?.name }), Promise.resolve({ nonNullName: () => Promise.reject(new Error('Oops')), }), - Promise.resolve({ nonNullName: friends[1].name }), + Promise.resolve({ nonNullName: friends[1]?.name }), ], }); expectJSON(result).toDeepEqual([ @@ -1050,9 +1050,9 @@ describe('Execute: stream directive', () => { `); const result = await complete(document, { nonNullFriendList: () => [ - { nonNullName: Promise.resolve(friends[0].name) }, + { nonNullName: Promise.resolve(friends[0]?.name) }, { nonNullName: Promise.reject(new Error('Oops')) }, - { nonNullName: Promise.resolve(friends[1].name) }, + { nonNullName: Promise.resolve(friends[1]?.name) }, ], }); expectJSON(result).toDeepEqual([ @@ -1090,11 +1090,11 @@ describe('Execute: stream directive', () => { `); const result = await complete(document, { async *friendList() { - yield await Promise.resolve({ nonNullName: friends[0].name }); + yield await Promise.resolve({ nonNullName: friends[0]?.name }); yield await Promise.resolve({ nonNullName: () => Promise.reject(new Error('Oops')), }); - yield await Promise.resolve({ nonNullName: friends[1].name }); + yield await Promise.resolve({ nonNullName: friends[1]?.name }); }, }); expectJSON(result).toDeepEqual([ @@ -1144,12 +1144,12 @@ describe('Execute: stream directive', () => { `); const result = await complete(document, { async *nonNullFriendList() { - yield await Promise.resolve({ nonNullName: friends[0].name }); + yield await Promise.resolve({ nonNullName: friends[0]?.name }); yield await Promise.resolve({ nonNullName: () => Promise.reject(new Error('Oops')), }); yield await Promise.resolve({ - nonNullName: friends[1].name, + nonNullName: friends[1]?.name, }); /* c8 ignore start */ } /* c8 ignore stop */, }); @@ -1380,7 +1380,7 @@ describe('Execute: stream directive', () => { const result = await complete(document, { async *friendList() { yield await Promise.resolve({ - name: friends[0].name, + name: friends[0]?.name, nonNullName: () => Promise.resolve(null), }); }, @@ -1430,7 +1430,7 @@ describe('Execute: stream directive', () => { return Promise.resolve({ done: false, value: { - name: friend.name, + name: friend?.name, nonNullName: null, }, }); @@ -1528,8 +1528,8 @@ describe('Execute: stream directive', () => { yield await Promise.resolve(friends[0]); yield await Promise.resolve(friends[1]); yield await Promise.resolve({ - id: friends[2].id, - name: () => Promise.resolve(friends[2].name), + id: friends[2]?.id, + name: () => Promise.resolve(friends[2]?.name), }); }, }); @@ -1678,7 +1678,7 @@ describe('Execute: stream directive', () => { async *friendList() { yield await Promise.resolve(friends[0]); yield await Promise.resolve({ - id: friends[1].id, + id: friends[1]?.id, name: () => slowFieldPromise, }); await iterableCompletionPromise; @@ -1764,7 +1764,7 @@ describe('Execute: stream directive', () => { async *friendList() { yield await Promise.resolve(friends[0]); yield await Promise.resolve({ - id: friends[1].id, + id: friends[1]?.id, name: () => slowFieldPromise, }); await iterableCompletionPromise; diff --git a/src/execution/__tests__/variables-test.ts b/src/execution/__tests__/variables-test.ts index 6e4b39e810..41da9d2e64 100644 --- a/src/execution/__tests__/variables-test.ts +++ b/src/execution/__tests__/variables-test.ts @@ -1074,7 +1074,7 @@ describe('Execute: Handles inputs', () => { `); const operation = doc.definitions[0]; - assert(operation.kind === Kind.OPERATION_DEFINITION); + assert(operation?.kind === Kind.OPERATION_DEFINITION); const { variableDefinitions } = operation; assert(variableDefinitions != null); diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 1bc6c4267b..e24f3a70cd 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -688,7 +688,12 @@ function executeField( asyncPayloadRecord?: AsyncPayloadRecord, ): PromiseOrValue { const errors = asyncPayloadRecord?.errors ?? exeContext.errors; - const fieldName = fieldNodes[0].name.value; + const firstFieldNode = fieldNodes[0]; + invariant(firstFieldNode !== undefined); + + const fieldName = fieldNodes[0]?.name.value; + invariant(fieldName !== undefined); + const fieldDef = exeContext.schema.getField(parentType, fieldName); if (!fieldDef) { return; @@ -712,7 +717,7 @@ function executeField( // TODO: find a way to memoize, in case this field is within a List type. const args = getArgumentValues( fieldDef, - fieldNodes[0], + firstFieldNode, exeContext.variableValues, ); @@ -974,11 +979,14 @@ function getStreamValues( return; } + const firstFieldNode = fieldNodes[0]; + invariant(firstFieldNode !== undefined); + // validation only allows equivalent streams on multiple fields, so it is // safe to only check the first fieldNode for the stream directive const stream = getDirectiveValues( GraphQLStreamDirective, - fieldNodes[0], + firstFieldNode, exeContext.variableValues, ); @@ -1497,16 +1505,19 @@ export const defaultTypeResolver: GraphQLTypeResolver = // Otherwise, test each possible type. const possibleTypes = info.schema.getPossibleTypes(abstractType); - const promisedIsTypeOfResults = []; + const promisedIsTypeOfResults: Array> = []; - for (let i = 0; i < possibleTypes.length; i++) { - const type = possibleTypes[i]; + let type: GraphQLObjectType; + for (type of possibleTypes) { if (type.isTypeOf) { const isTypeOfResult = type.isTypeOf(value, contextValue, info); if (isPromise(isTypeOfResult)) { - promisedIsTypeOfResults[i] = isTypeOfResult; + const possibleTypeName = type.name; + promisedIsTypeOfResults.push( + isTypeOfResult.then((result) => [possibleTypeName, result]), + ); } else if (isTypeOfResult) { return type.name; } @@ -1514,10 +1525,11 @@ export const defaultTypeResolver: GraphQLTypeResolver = } if (promisedIsTypeOfResults.length) { + // QUESTION: Can this be faster if Promise.any or Promise.race is used instead? return Promise.all(promisedIsTypeOfResults).then((isTypeOfResults) => { - for (let i = 0; i < isTypeOfResults.length; i++) { - if (isTypeOfResults[i]) { - return possibleTypes[i].name; + for (const [name, result] of isTypeOfResults) { + if (result) { + return name; } } }); diff --git a/src/jsutils/formatList.ts b/src/jsutils/formatList.ts index 85d8a1738f..8516f18612 100644 --- a/src/jsutils/formatList.ts +++ b/src/jsutils/formatList.ts @@ -15,13 +15,14 @@ export function andList(items: ReadonlyArray): string { } function formatList(conjunction: string, items: ReadonlyArray): string { - invariant(items.length !== 0); + const firstItem = items[0]; + invariant(firstItem !== undefined); switch (items.length) { case 1: - return items[0]; + return firstItem; case 2: - return items[0] + ' ' + conjunction + ' ' + items[1]; + return firstItem + ' ' + conjunction + ' ' + items[1]; } const allButLast = items.slice(0, -1); diff --git a/src/jsutils/mapValue.ts b/src/jsutils/mapValue.ts index 8f1299586e..46edc79d16 100644 --- a/src/jsutils/mapValue.ts +++ b/src/jsutils/mapValue.ts @@ -8,10 +8,8 @@ export function mapValue( map: ReadOnlyObjMap, fn: (value: T, key: string) => V, ): ObjMap { - const result = Object.create(null); - - for (const key of Object.keys(map)) { - result[key] = fn(map[key], key); - } - return result; + return Object.entries(map).reduce((accumulator, [key, value]) => { + accumulator[key] = fn(value, key); + return accumulator; + }, Object.create(null)); } diff --git a/src/jsutils/promiseForObject.ts b/src/jsutils/promiseForObject.ts index ff48d9f218..b7314d16d8 100644 --- a/src/jsutils/promiseForObject.ts +++ b/src/jsutils/promiseForObject.ts @@ -1,3 +1,4 @@ +import { invariant } from './invariant.js'; import type { ObjMap } from './ObjMap.js'; /** @@ -15,8 +16,12 @@ export async function promiseForObject( const resolvedValues = await Promise.all(values); const resolvedObject = Object.create(null); + for (let i = 0; i < keys.length; ++i) { - resolvedObject[keys[i]] = resolvedValues[i]; + const key = keys[i]; + invariant(key !== undefined); + + resolvedObject[key] = resolvedValues[i]; } return resolvedObject; } diff --git a/src/jsutils/suggestionList.ts b/src/jsutils/suggestionList.ts index 4114ddad39..8ac8ad6672 100644 --- a/src/jsutils/suggestionList.ts +++ b/src/jsutils/suggestionList.ts @@ -1,3 +1,4 @@ +import { invariant } from './invariant.js'; import { naturalCompare } from './naturalCompare.js'; /** @@ -93,19 +94,34 @@ class LexicalDistance { const upRow = rows[(i - 1) % 3]; const currentRow = rows[i % 3]; + invariant(upRow !== undefined); + invariant(currentRow !== undefined); + let smallestCell = (currentRow[0] = i); for (let j = 1; j <= bLength; j++) { const cost = a[i - 1] === b[j - 1] ? 0 : 1; + const deleteTarget = upRow[j]; + const currentRowTarget = currentRow[j - 1]; + const substituteTarget = upRow[j - 1]; + + invariant(deleteTarget !== undefined); + invariant(currentRowTarget !== undefined); + invariant(substituteTarget !== undefined); + let currentCell = Math.min( - upRow[j] + 1, // delete - currentRow[j - 1] + 1, // insert - upRow[j - 1] + cost, // substitute + deleteTarget + 1, // delete + currentRowTarget + 1, // insert + substituteTarget + cost, // substitute ); if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) { // transposition - const doubleDiagonalCell = rows[(i - 2) % 3][j - 2]; + const targetedRow = rows[(i - 2) % 3]; + invariant(targetedRow !== undefined); + const doubleDiagonalCell = targetedRow[j - 2]; + invariant(doubleDiagonalCell !== undefined); + currentCell = Math.min(currentCell, doubleDiagonalCell + 1); } @@ -122,8 +138,15 @@ class LexicalDistance { } } - const distance = rows[aLength % 3][bLength]; - return distance <= threshold ? distance : undefined; + const targetedRow = rows[aLength % 3]; + + invariant(targetedRow !== undefined); + + const distance = targetedRow[bLength]; + + return distance !== undefined && distance <= threshold + ? distance + : undefined; } } diff --git a/src/language/blockString.ts b/src/language/blockString.ts index 3d7915b781..7cb046d4bc 100644 --- a/src/language/blockString.ts +++ b/src/language/blockString.ts @@ -15,12 +15,11 @@ export function dedentBlockStringLines( let firstNonEmptyLine = null; let lastNonEmptyLine = -1; - for (let i = 0; i < lines.length; ++i) { - const line = lines[i]; + lines.forEach((line, i) => { const indent = leadingWhitespace(line); if (indent === line.length) { - continue; // skip empty lines + return; // skip empty lines } firstNonEmptyLine ??= i; @@ -29,7 +28,7 @@ export function dedentBlockStringLines( if (i !== 0 && indent < commonIndent) { commonIndent = indent; } - } + }); return ( lines diff --git a/src/language/printLocation.ts b/src/language/printLocation.ts index b6f9c78401..578f32b142 100644 --- a/src/language/printLocation.ts +++ b/src/language/printLocation.ts @@ -1,3 +1,5 @@ +import { invariant } from '../jsutils/invariant.js'; + import type { Location } from './ast.js'; import type { SourceLocation } from './location.js'; import { getLocation } from './location.js'; @@ -35,7 +37,7 @@ export function printSourceLocation( const locationLine = lines[lineIndex]; // Special case for minified documents - if (locationLine.length > 120) { + if (locationLine !== undefined && locationLine.length > 120) { const subLineIndex = Math.floor(columnNum / 80); const subLineColumnNum = columnNum % 80; const subLines: Array = []; @@ -43,27 +45,45 @@ export function printSourceLocation( subLines.push(locationLine.slice(i, i + 80)); } + const firstSubLine = subLines[0]; + const nextSubLines = subLines.slice(1, subLineIndex + 1); + const nextSubLine = subLines[subLineIndex + 1]; + + invariant(firstSubLine !== undefined); + // invariant(nextSubLine !== undefined); + return ( locationStr + printPrefixedLines([ - [`${lineNum} |`, subLines[0]], - ...subLines - .slice(1, subLineIndex + 1) - .map((subLine) => ['|', subLine] as const), + [`${lineNum} |`, firstSubLine], + ...nextSubLines.map<[string, string]>((subLine) => ['|', subLine]), ['|', '^'.padStart(subLineColumnNum)], - ['|', subLines[subLineIndex + 1]], + // TODO: This assertion can be removed if the above invariant is comment in. + ['|', nextSubLine as string], ]) ); } + const previousLine = lines[lineIndex - 1]; + const nextLine = lines[lineIndex + 1]; + + // TODO: With the way the types are set up, we should be able to + // comment these in, but doing so breaks tests. + // + // invariant(previousLine !== undefined); + // invariant(nextLine !== undefined); + invariant(locationLine !== undefined); + return ( locationStr + printPrefixedLines([ // Lines specified like this: ["prefix", "string"], - [`${lineNum - 1} |`, lines[lineIndex - 1]], + // TODO: This assertion can be removed if the above invariant is comment in. + [`${lineNum - 1} |`, previousLine as string], [`${lineNum} |`, locationLine], ['|', '^'.padStart(columnNum)], - [`${lineNum + 1} |`, lines[lineIndex + 1]], + // TODO: This assertion can be removed if the above invariant is comment in. + [`${lineNum + 1} |`, nextLine as string], ]) ); } diff --git a/src/language/printString.ts b/src/language/printString.ts index b091bcc2c1..c5019c4198 100644 --- a/src/language/printString.ts +++ b/src/language/printString.ts @@ -1,3 +1,5 @@ +import { invariant } from '../jsutils/invariant.js'; + /** * Prints a string as a GraphQL StringValue literal. Replaces control characters * and excluded characters (" U+0022 and \\ U+005C) with escape sequences. @@ -10,7 +12,15 @@ export function printString(str: string): string { const escapedRegExp = /[\x00-\x1f\x22\x5c\x7f-\x9f]/g; function escapedReplacer(str: string): string { - return escapeSequences[str.charCodeAt(0)]; + const firstCharacter = str.charCodeAt(0); + + invariant(firstCharacter !== undefined); + + const replacer = escapeSequences[firstCharacter]; + + invariant(replacer !== undefined); + + return replacer; } // prettier-ignore diff --git a/src/language/visitor.ts b/src/language/visitor.ts index 86fc48a826..1cc72cd3df 100644 --- a/src/language/visitor.ts +++ b/src/language/visitor.ts @@ -319,12 +319,12 @@ export function visitInParallel( const enterList = new Array(visitors.length).fill(undefined); const leaveList = new Array(visitors.length).fill(undefined); - for (let i = 0; i < visitors.length; ++i) { - const { enter, leave } = getEnterLeaveForKind(visitors[i], kind); + visitors.forEach((visitor, i) => { + const { enter, leave } = getEnterLeaveForKind(visitor, kind); hasVisitor ||= enter != null || leave != null; enterList[i] = enter; leaveList[i] = leave; - } + }); if (!hasVisitor) { continue; diff --git a/src/type/__tests__/extensions-test.ts b/src/type/__tests__/extensions-test.ts index 8bfdfb7b76..ed3e5a00f4 100644 --- a/src/type/__tests__/extensions-test.ts +++ b/src/type/__tests__/extensions-test.ts @@ -62,17 +62,17 @@ describe('Type System: Extensions', () => { expect(someObject.extensions).to.deep.equal({}); const someField = someObject.getFields().someField; - expect(someField.extensions).to.deep.equal({}); - const someArg = someField.args[0]; - expect(someArg.extensions).to.deep.equal({}); + expect(someField?.extensions).to.deep.equal({}); + const someArg = someField?.args[0]; + expect(someArg?.extensions).to.deep.equal({}); const config = someObject.toConfig(); expect(config.extensions).to.deep.equal({}); const someFieldConfig = config.fields.someField; - expect(someFieldConfig.extensions).to.deep.equal({}); - assert(someFieldConfig.args != null); + expect(someFieldConfig?.extensions).to.deep.equal({}); + assert(someFieldConfig?.args != null); const someArgConfig = someFieldConfig.args.someArg; - expect(someArgConfig.extensions).to.deep.equal({}); + expect(someArgConfig?.extensions).to.deep.equal({}); }); it('with extensions', () => { @@ -99,17 +99,17 @@ describe('Type System: Extensions', () => { expectObjMap(someObject.extensions).to.deep.equal(objectExtensions); const someField = someObject.getFields().someField; - expectObjMap(someField.extensions).to.deep.equal(fieldExtensions); - const someArg = someField.args[0]; - expectObjMap(someArg.extensions).to.deep.equal(argExtensions); + expectObjMap(someField?.extensions).to.deep.equal(fieldExtensions); + const someArg = someField?.args[0]; + expectObjMap(someArg?.extensions).to.deep.equal(argExtensions); const config = someObject.toConfig(); expectObjMap(config.extensions).to.deep.equal(objectExtensions); const someFieldConfig = config.fields.someField; - expectObjMap(someFieldConfig.extensions).to.deep.equal(fieldExtensions); - assert(someFieldConfig.args != null); + expectObjMap(someFieldConfig?.extensions).to.deep.equal(fieldExtensions); + assert(someFieldConfig?.args != null); const someArgConfig = someFieldConfig.args.someArg; - expectObjMap(someArgConfig.extensions).to.deep.equal(argExtensions); + expectObjMap(someArgConfig?.extensions).to.deep.equal(argExtensions); }); }); @@ -131,17 +131,17 @@ describe('Type System: Extensions', () => { expect(someInterface.extensions).to.deep.equal({}); const someField = someInterface.getFields().someField; - expect(someField.extensions).to.deep.equal({}); - const someArg = someField.args[0]; - expect(someArg.extensions).to.deep.equal({}); + expect(someField?.extensions).to.deep.equal({}); + const someArg = someField?.args[0]; + expect(someArg?.extensions).to.deep.equal({}); const config = someInterface.toConfig(); expect(config.extensions).to.deep.equal({}); const someFieldConfig = config.fields.someField; - expect(someFieldConfig.extensions).to.deep.equal({}); - assert(someFieldConfig.args != null); + expect(someFieldConfig?.extensions).to.deep.equal({}); + assert(someFieldConfig?.args != null); const someArgConfig = someFieldConfig.args.someArg; - expect(someArgConfig.extensions).to.deep.equal({}); + expect(someArgConfig?.extensions).to.deep.equal({}); }); it('with extensions', () => { @@ -170,17 +170,17 @@ describe('Type System: Extensions', () => { expectObjMap(someInterface.extensions).to.deep.equal(interfaceExtensions); const someField = someInterface.getFields().someField; - expectObjMap(someField.extensions).to.deep.equal(fieldExtensions); - const someArg = someField.args[0]; - expectObjMap(someArg.extensions).to.deep.equal(argExtensions); + expectObjMap(someField?.extensions).to.deep.equal(fieldExtensions); + const someArg = someField?.args[0]; + expectObjMap(someArg?.extensions).to.deep.equal(argExtensions); const config = someInterface.toConfig(); expectObjMap(config.extensions).to.deep.equal(interfaceExtensions); const someFieldConfig = config.fields.someField; - expectObjMap(someFieldConfig.extensions).to.deep.equal(fieldExtensions); - assert(someFieldConfig.args != null); + expectObjMap(someFieldConfig?.extensions).to.deep.equal(fieldExtensions); + assert(someFieldConfig?.args != null); const someArgConfig = someFieldConfig.args.someArg; - expectObjMap(someArgConfig.extensions).to.deep.equal(argExtensions); + expectObjMap(someArgConfig?.extensions).to.deep.equal(argExtensions); }); }); @@ -224,12 +224,12 @@ describe('Type System: Extensions', () => { expect(someEnum.extensions).to.deep.equal({}); const someValue = someEnum.getValues()[0]; - expect(someValue.extensions).to.deep.equal({}); + expect(someValue?.extensions).to.deep.equal({}); const config = someEnum.toConfig(); expect(config.extensions).to.deep.equal({}); const someValueConfig = config.values.SOME_VALUE; - expect(someValueConfig.extensions).to.deep.equal({}); + expect(someValueConfig?.extensions).to.deep.equal({}); }); it('with extensions', () => { @@ -248,12 +248,12 @@ describe('Type System: Extensions', () => { expectObjMap(someEnum.extensions).to.deep.equal(enumExtensions); const someValue = someEnum.getValues()[0]; - expectObjMap(someValue.extensions).to.deep.equal(valueExtensions); + expectObjMap(someValue?.extensions).to.deep.equal(valueExtensions); const config = someEnum.toConfig(); expectObjMap(config.extensions).to.deep.equal(enumExtensions); const someValueConfig = config.values.SOME_VALUE; - expectObjMap(someValueConfig.extensions).to.deep.equal(valueExtensions); + expectObjMap(someValueConfig?.extensions).to.deep.equal(valueExtensions); }); }); @@ -270,12 +270,12 @@ describe('Type System: Extensions', () => { expect(someInputObject.extensions).to.deep.equal({}); const someInputField = someInputObject.getFields().someInputField; - expect(someInputField.extensions).to.deep.equal({}); + expect(someInputField?.extensions).to.deep.equal({}); const config = someInputObject.toConfig(); expect(config.extensions).to.deep.equal({}); const someInputFieldConfig = config.fields.someInputField; - expect(someInputFieldConfig.extensions).to.deep.equal({}); + expect(someInputFieldConfig?.extensions).to.deep.equal({}); }); it('with extensions', () => { @@ -301,14 +301,14 @@ describe('Type System: Extensions', () => { inputObjectExtensions, ); const someInputField = someInputObject.getFields().someInputField; - expectObjMap(someInputField.extensions).to.deep.equal( + expectObjMap(someInputField?.extensions).to.deep.equal( inputFieldExtensions, ); const config = someInputObject.toConfig(); expectObjMap(config.extensions).to.deep.equal(inputObjectExtensions); const someInputFieldConfig = config.fields.someInputField; - expectObjMap(someInputFieldConfig.extensions).to.deep.equal( + expectObjMap(someInputFieldConfig?.extensions).to.deep.equal( inputFieldExtensions, ); }); @@ -328,12 +328,12 @@ describe('Type System: Extensions', () => { expect(someDirective.extensions).to.deep.equal({}); const someArg = someDirective.args[0]; - expect(someArg.extensions).to.deep.equal({}); + expect(someArg?.extensions).to.deep.equal({}); const config = someDirective.toConfig(); expect(config.extensions).to.deep.equal({}); const someArgConfig = config.args.someArg; - expect(someArgConfig.extensions).to.deep.equal({}); + expect(someArgConfig?.extensions).to.deep.equal({}); }); it('with extensions', () => { @@ -356,12 +356,12 @@ describe('Type System: Extensions', () => { expectObjMap(someDirective.extensions).to.deep.equal(directiveExtensions); const someArg = someDirective.args[0]; - expectObjMap(someArg.extensions).to.deep.equal(argExtensions); + expectObjMap(someArg?.extensions).to.deep.equal(argExtensions); const config = someDirective.toConfig(); expectObjMap(config.extensions).to.deep.equal(directiveExtensions); const someArgConfig = config.args.someArg; - expectObjMap(someArgConfig.extensions).to.deep.equal(argExtensions); + expectObjMap(someArgConfig?.extensions).to.deep.equal(argExtensions); }); }); diff --git a/src/utilities/__tests__/TypeInfo-test.ts b/src/utilities/__tests__/TypeInfo-test.ts index 971316f8b4..a7d293722e 100644 --- a/src/utilities/__tests__/TypeInfo-test.ts +++ b/src/utilities/__tests__/TypeInfo-test.ts @@ -465,11 +465,11 @@ describe('visitWithTypeInfo', () => { const ast = parse('{ name, pets { name } }'); const operationNode = ast.definitions[0]; - assert(operationNode.kind === 'OperationDefinition'); + assert(operationNode?.kind === 'OperationDefinition'); const visited: Array = []; visit( - operationNode.selectionSet, + operationNode?.selectionSet, visitWithTypeInfo(typeInfo, { enter(node) { const parentType = typeInfo.getParentType(); diff --git a/src/utilities/__tests__/buildASTSchema-test.ts b/src/utilities/__tests__/buildASTSchema-test.ts index 6e152af36c..34ce801589 100644 --- a/src/utilities/__tests__/buildASTSchema-test.ts +++ b/src/utilities/__tests__/buildASTSchema-test.ts @@ -333,7 +333,7 @@ describe('Schema Builder', () => { const definition = parse(sdl).definitions[0]; expect( - definition.kind === 'InterfaceTypeDefinition' && definition.interfaces, + definition?.kind === 'InterfaceTypeDefinition' && definition.interfaces, ).to.deep.equal([], 'The interfaces property must be an empty array.'); expect(cycleSDL(sdl)).to.equal(sdl); @@ -682,12 +682,12 @@ describe('Schema Builder', () => { deprecationReason: 'Use newInput', }); - const field3OldArg = rootFields.field3.args[0]; + const field3OldArg = rootFields.field3?.args[0]; expect(field3OldArg).to.include({ deprecationReason: 'No longer supported', }); - const field4OldArg = rootFields.field4.args[0]; + const field4OldArg = rootFields.field4?.args[0]; expect(field4OldArg).to.include({ deprecationReason: 'Why not?', }); @@ -981,7 +981,7 @@ describe('Schema Builder', () => { expectASTNode(testField).to.equal( 'testField(testArg: TestInput): TestUnion', ); - expectASTNode(testField.args[0]).to.equal('testArg: TestInput'); + expectASTNode(testField?.args[0]).to.equal('testArg: TestInput'); expectASTNode(testInput.getFields().testInputField).to.equal( 'testInputField: TestEnum', ); diff --git a/src/utilities/__tests__/buildClientSchema-test.ts b/src/utilities/__tests__/buildClientSchema-test.ts index 209943e18e..2cee9e622d 100644 --- a/src/utilities/__tests__/buildClientSchema-test.ts +++ b/src/utilities/__tests__/buildClientSchema-test.ts @@ -765,8 +765,8 @@ describe('Type System: build schema from introspection', () => { ); assert(queryTypeIntrospection?.kind === 'OBJECT'); - const argType = queryTypeIntrospection.fields[0].args[0].type; - assert(argType.kind === 'SCALAR'); + const argType = queryTypeIntrospection.fields[0]?.args[0]?.type; + assert(argType?.kind === 'SCALAR'); expect(argType).to.have.property('name', 'String'); // @ts-expect-error @@ -784,8 +784,8 @@ describe('Type System: build schema from introspection', () => { ); assert(queryTypeIntrospection?.kind === 'OBJECT'); - const fieldType = queryTypeIntrospection.fields[0].type; - assert(fieldType.kind === 'SCALAR'); + const fieldType = queryTypeIntrospection.fields[0]?.type; + assert(fieldType?.kind === 'SCALAR'); expect(fieldType).to.have.property('name', 'String'); // @ts-expect-error diff --git a/src/utilities/__tests__/extendSchema-test.ts b/src/utilities/__tests__/extendSchema-test.ts index 5e2d786f22..afb09d09c6 100644 --- a/src/utilities/__tests__/extendSchema-test.ts +++ b/src/utilities/__tests__/extendSchema-test.ts @@ -500,7 +500,7 @@ describe('extendSchema', () => { const newField = query.getFields().newField; expectASTNode(newField).to.equal('newField(testArg: TestInput): TestEnum'); - expectASTNode(newField.args[0]).to.equal('testArg: TestInput'); + expectASTNode(newField?.args[0]).to.equal('testArg: TestInput'); expectASTNode(query.getFields().oneMoreNewField).to.equal( 'oneMoreNewField: TestUnion', ); diff --git a/src/utilities/lexicographicSortSchema.ts b/src/utilities/lexicographicSortSchema.ts index 4675185a4e..4cba83601d 100644 --- a/src/utilities/lexicographicSortSchema.ts +++ b/src/utilities/lexicographicSortSchema.ts @@ -166,8 +166,11 @@ function sortObjMap( sortValueFn: (value: T) => R, ): ObjMap { const sortedMap = Object.create(null); - for (const key of Object.keys(map).sort(naturalCompare)) { - sortedMap[key] = sortValueFn(map[key]); + + for (const [key, value] of Object.entries(map).sort( + ([firstKey], [secondKey]) => naturalCompare(firstKey, secondKey), + )) { + sortedMap[key] = sortValueFn(value); } return sortedMap; } diff --git a/src/validation/__tests__/FieldsOnCorrectTypeRule-test.ts b/src/validation/__tests__/FieldsOnCorrectTypeRule-test.ts index 1c7fbc0351..514c5b8c75 100644 --- a/src/validation/__tests__/FieldsOnCorrectTypeRule-test.ts +++ b/src/validation/__tests__/FieldsOnCorrectTypeRule-test.ts @@ -295,7 +295,7 @@ describe('Validate: Fields on correct type', () => { FieldsOnCorrectTypeRule, ]); expect(errors.length).to.equal(1); - return expect(errors[0].message); + return expect(errors[0]?.message); } it('Works with no suggestions', () => { diff --git a/src/validation/rules/DeferStreamDirectiveOnValidOperationsRule.ts b/src/validation/rules/DeferStreamDirectiveOnValidOperationsRule.ts index e8e6a292b6..0b91f7c07b 100644 --- a/src/validation/rules/DeferStreamDirectiveOnValidOperationsRule.ts +++ b/src/validation/rules/DeferStreamDirectiveOnValidOperationsRule.ts @@ -1,3 +1,5 @@ +import { invariant } from '../../jsutils/invariant.js'; + import { GraphQLError } from '../../error/GraphQLError.js'; import type { DirectiveNode } from '../../language/ast.js'; @@ -50,6 +52,8 @@ export function DeferStreamDirectiveOnValidOperationsRule( Directive(node, _key, _parent, _path, ancestors) { const definitionNode = ancestors[2]; + invariant(definitionNode !== undefined); + if ( 'kind' in definitionNode && ((definitionNode.kind === Kind.FRAGMENT_DEFINITION && diff --git a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts index e2444047c6..39487528ec 100644 --- a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts +++ b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts @@ -1,4 +1,5 @@ import { inspect } from '../../jsutils/inspect.js'; +import { invariant } from '../../jsutils/invariant.js'; import type { Maybe } from '../../jsutils/Maybe.js'; import type { ObjMap } from '../../jsutils/ObjMap.js'; @@ -199,6 +200,9 @@ function findConflictsWithinSelectionSet( // (B) Then collect conflicts between these fields and those represented by // each spread fragment name found. for (let i = 0; i < fragmentNames.length; i++) { + const firstFragment = fragmentNames[i]; + invariant(firstFragment !== undefined); + collectConflictsBetweenFieldsAndFragment( context, conflicts, @@ -206,21 +210,25 @@ function findConflictsWithinSelectionSet( comparedFragmentPairs, false, fieldMap, - fragmentNames[i], + firstFragment, ); // (C) Then compare this fragment with all other fragments found in this // selection set to collect conflicts between fragments spread together. // This compares each item in the list of fragment names to every other // item in that same list (except for itself). + for (let j = i + 1; j < fragmentNames.length; j++) { + const secondFragment = fragmentNames[j]; + invariant(secondFragment !== undefined); + collectConflictsBetweenFragments( context, conflicts, cachedFieldsAndFragmentNames, comparedFragmentPairs, false, - fragmentNames[i], - fragmentNames[j], + firstFragment, + secondFragment, ); } } @@ -491,15 +499,21 @@ function collectConflictsWithin( // be compared. if (fields.length > 1) { for (let i = 0; i < fields.length; i++) { + const firstField = fields[i]; + invariant(firstField !== undefined); + for (let j = i + 1; j < fields.length; j++) { + const secondField = fields[j]; + invariant(secondField !== undefined); + const conflict = findConflict( context, cachedFieldsAndFragmentNames, comparedFragmentPairs, false, // within one collection is never mutually exclusive responseName, - fields[i], - fields[j], + firstField, + secondField, ); if (conflict) { conflicts.push(conflict); @@ -784,10 +798,10 @@ function _collectFieldsAndFragmentNames( const responseName = selection.alias ? selection.alias.value : fieldName; - if (!nodeAndDefs[responseName]) { - nodeAndDefs[responseName] = []; - } - nodeAndDefs[responseName].push([parentType, selection, fieldDef]); + + const nodeAndDefsTarget = nodeAndDefs[responseName] ?? []; + nodeAndDefs[responseName] = nodeAndDefsTarget; + nodeAndDefsTarget.push([parentType, selection, fieldDef]); break; } case Kind.FRAGMENT_SPREAD: diff --git a/src/validation/rules/SingleFieldSubscriptionsRule.ts b/src/validation/rules/SingleFieldSubscriptionsRule.ts index 4a3d834124..03e8e46a96 100644 --- a/src/validation/rules/SingleFieldSubscriptionsRule.ts +++ b/src/validation/rules/SingleFieldSubscriptionsRule.ts @@ -62,8 +62,8 @@ export function SingleFieldSubscriptionsRule( ); } for (const fieldNodes of fields.values()) { - const fieldName = fieldNodes[0].name.value; - if (fieldName.startsWith('__')) { + const fieldName = fieldNodes[0]?.name.value; + if (fieldName?.startsWith('__')) { context.reportError( new GraphQLError( operationName != null diff --git a/src/validation/rules/UniqueInputFieldNamesRule.ts b/src/validation/rules/UniqueInputFieldNamesRule.ts index 7344e9439b..6c18f122d4 100644 --- a/src/validation/rules/UniqueInputFieldNamesRule.ts +++ b/src/validation/rules/UniqueInputFieldNamesRule.ts @@ -36,11 +36,12 @@ export function UniqueInputFieldNamesRule( }, ObjectField(node) { const fieldName = node.name.value; - if (knownNames[fieldName]) { + const target = knownNames[fieldName]; + if (target) { context.reportError( new GraphQLError( `There can be only one input field named "${fieldName}".`, - { nodes: [knownNames[fieldName], node.name] }, + { nodes: [target, node.name] }, ), ); } else { diff --git a/src/validation/rules/custom/NoDeprecatedCustomRule.ts b/src/validation/rules/custom/NoDeprecatedCustomRule.ts index 375373eb1d..f11db26b0f 100644 --- a/src/validation/rules/custom/NoDeprecatedCustomRule.ts +++ b/src/validation/rules/custom/NoDeprecatedCustomRule.ts @@ -64,10 +64,11 @@ export function NoDeprecatedCustomRule(context: ValidationContext): ASTVisitor { if (isInputObjectType(inputObjectDef)) { const inputFieldDef = inputObjectDef.getFields()[node.name.value]; const deprecationReason = inputFieldDef?.deprecationReason; - if (deprecationReason != null) { + if (inputFieldDef && deprecationReason != null) { + const { name } = inputFieldDef; context.reportError( new GraphQLError( - `The input field ${inputObjectDef.name}.${inputFieldDef.name} is deprecated. ${deprecationReason}`, + `The input field ${inputObjectDef.name}.${name} is deprecated. ${deprecationReason}`, { nodes: node }, ), ); diff --git a/tsconfig.json b/tsconfig.json index 51fd80f4e0..4c92db89d5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,7 +30,7 @@ "noImplicitOverride": true, "noImplicitReturns": false, // TODO consider "noPropertyAccessFromIndexSignature": false, // TODO consider - "noUncheckedIndexedAccess": false, // FIXME + "noUncheckedIndexedAccess": true, "noUnusedLocals": true, "noUnusedParameters": true, "allowSyntheticDefaultImports": true