From 6b4aa8a43d1c9d60460c39bc4077404380bb8699 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 18 Jun 2024 08:12:18 -0500 Subject: [PATCH] fix: compile error TS2589 - Type instantiation is excessively deep and possibly infinite --- src/sfProject.ts | 11 ++-- src/util/findUppercaseKeys.ts | 38 ++++++++------ test/unit/util/findUppercaseKeysTest.ts | 70 ++++++++++++++++++++----- 3 files changed, 82 insertions(+), 37 deletions(-) diff --git a/src/sfProject.ts b/src/sfProject.ts index 80002cf8b9..afc5a604dd 100644 --- a/src/sfProject.ts +++ b/src/sfProject.ts @@ -24,13 +24,11 @@ import { resolveProjectPath, resolveProjectPathSync, SFDX_PROJECT_JSON } from '. import { SfError } from './sfError'; import { Messages } from './messages'; -import { findUpperCaseKeys } from './util/findUppercaseKeys'; +import { ensureNoUppercaseKeys } from './util/findUppercaseKeys'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/core', 'config'); -const coreMessages = Messages.loadMessages('@salesforce/core', 'core'); - /** @deprecated. Use PackageDirDependency from @salesforce/schemas */ export type PackageDirDependency = PackageDirDependencySchema; @@ -70,6 +68,7 @@ export type ProjectJson = ConfigContents & ProjectJsonSchema; * **See** [force:project:create](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_ws_create_new.htm) */ export class SfProjectJson extends ConfigFile { + /** json properties that are uppercase, or allow uppercase keys inside them */ public static BLOCKLIST = ['packageAliases']; public static getFileName(): string { @@ -338,11 +337,7 @@ export class SfProjectJson extends ConfigFile { } private validateKeys(): void { - // Verify that the configObject does not have upper case keys; throw if it does. Must be heads down camel case. - const upperCaseKey = findUpperCaseKeys(this.toObject(), SfProjectJson.BLOCKLIST); - if (upperCaseKey) { - throw coreMessages.createError('invalidJsonCasing', [upperCaseKey, this.getPath()]); - } + ensureNoUppercaseKeys(this.getPath())(SfProjectJson.BLOCKLIST)(this.toObject()); } } diff --git a/src/util/findUppercaseKeys.ts b/src/util/findUppercaseKeys.ts index 3452647d56..839a316e80 100644 --- a/src/util/findUppercaseKeys.ts +++ b/src/util/findUppercaseKeys.ts @@ -5,21 +5,25 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { JsonMap, Optional, isJsonMap, asJsonMap, AnyJson } from '@salesforce/ts-types'; -import { findKey } from '@salesforce/kit'; +import { strictEqual } from 'node:assert/strict'; +import { JsonMap, isJsonMap } from '@salesforce/ts-types'; +import { Messages } from '../messages'; -export const findUpperCaseKeys = (data?: JsonMap, sectionBlocklist: string[] = []): Optional => { - let key: Optional; - findKey(data, (val: AnyJson, k: string) => { - if (/^[A-Z]/.test(k)) { - key = k; - } else if (isJsonMap(val)) { - if (sectionBlocklist.includes(k)) { - return key; - } - key = findUpperCaseKeys(asJsonMap(val)); - } - return key; - }); - return key; -}; +Messages.importMessagesDirectory(__dirname); +const coreMessages = Messages.loadMessages('@salesforce/core', 'core'); + +/** will throw on any upperCase unless they are present in the allowList. Recursively searches the object, returning valid keys */ +export const ensureNoUppercaseKeys = + (path: string) => + (allowList: string[] = []) => + (data: JsonMap): string[] => { + const keys = getKeys(data, allowList); + const upperCaseKeys = keys.filter((key) => /^[A-Z]/.test(key)).join(', '); + strictEqual(upperCaseKeys.length, 0, coreMessages.getMessage('invalidJsonCasing', [upperCaseKeys, path])); + return keys; + }; + +const getKeys = (data: JsonMap, allowList: string[]): string[] => + Object.entries(data) + .filter(([k]) => !allowList.includes(k)) + .flatMap(([key, value]) => (isJsonMap(value) ? [key, ...getKeys(value, allowList)] : [key])); diff --git a/test/unit/util/findUppercaseKeysTest.ts b/test/unit/util/findUppercaseKeysTest.ts index 27b91a7d6b..12b73382e1 100644 --- a/test/unit/util/findUppercaseKeysTest.ts +++ b/test/unit/util/findUppercaseKeysTest.ts @@ -5,44 +5,90 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { expect } from 'chai'; -import { findUpperCaseKeys } from '../../../src/util/findUppercaseKeys'; +import { expect, assert } from 'chai'; +import { JsonMap } from '@salesforce/ts-types'; +import { ensureNoUppercaseKeys } from '../../../src/util/findUppercaseKeys'; describe('findUpperCaseKeys', () => { - it('should return the first upper case key', () => { + const fn = ensureNoUppercaseKeys('testPath')(); + + const messageFromFailure = (obj: JsonMap): string => { + const failMessage = 'should have thrown'; + try { + fn(obj); + expect.fail(failMessage); + } catch (e) { + assert(e instanceof Error); + assert(e.message !== failMessage); + return e.message; + } + }; + + it('should throw on top-level uppercase keys', () => { + const testObj = { + lowercase: true, + UpperCase: false, + nested: { camelCase: true }, + }; + expect(messageFromFailure(testObj)).to.include('UpperCase'); + }); + + it('should throw with multiple uppercase keys', () => { const testObj = { lowercase: true, UpperCase: false, nested: { camelCase: true }, + AnotherUpperCase: false, + }; + const msg = messageFromFailure(testObj); + expect(msg).to.include('UpperCase'); + expect(msg).to.include('AnotherUpperCase'); + }); + + it('should throw with multiple uppercase keys when one is nested', () => { + const testObj = { + lowercase: true, + UpperCase: false, + nested: { camelCase: true, AnotherUpperCase: true }, }; - expect(findUpperCaseKeys(testObj)).to.equal('UpperCase'); + const msg = messageFromFailure(testObj); + expect(msg).to.include('UpperCase'); + expect(msg).to.include('AnotherUpperCase'); }); - it('should return the first nested upper case key', () => { + it('should throw if nested uppercase keys', () => { const testObj = { lowercase: true, uppercase: false, nested: { NestedUpperCase: true }, }; - expect(findUpperCaseKeys(testObj)).to.equal('NestedUpperCase'); + expect(messageFromFailure(testObj)).to.include('NestedUpperCase'); }); - it('should return undefined when no upper case key is found', () => { + it('returns all non-blocked keys when no uppercase keys are found', () => { const testObj = { lowercase: true, uppercase: false, nested: { camelCase: true }, }; - expect(findUpperCaseKeys(testObj)).to.be.undefined; + expect(fn(testObj)).to.deep.equal(['lowercase', 'uppercase', 'nested', 'camelCase']); }); - it('should return the first nested upper case key unless blocklisted', () => { + it('allowList can have uppercase keys inside it', () => { const testObj = { lowercase: true, uppercase: false, nested: { NestedUpperCase: true }, }; - expect(findUpperCaseKeys(testObj, ['nested'])).to.equal(undefined); + expect(ensureNoUppercaseKeys('testPath')(['nested'])(testObj)).to.deep.equal(['lowercase', 'uppercase']); + }); + + it('allowList can have top-level uppercase keys', () => { + const testObj = { + lowercase: true, + TopLevel: false, + }; + expect(ensureNoUppercaseKeys('testPath')(['TopLevel'])(testObj)).to.deep.equal(['lowercase']); }); it('handles keys starting with numbers', () => { @@ -51,13 +97,13 @@ describe('findUpperCaseKeys', () => { Abc: false, nested: { '2abc': true }, }; - expect(findUpperCaseKeys(testObj)).to.equal('Abc'); + expect(messageFromFailure(testObj)).to.include('Abc'); }); it('handles keys starting with numbers', () => { const testObj = { '1abc': true, nested: { '2abc': true, Upper: false }, }; - expect(findUpperCaseKeys(testObj)).to.equal('Upper'); + expect(messageFromFailure(testObj)).to.include('Upper'); }); });