diff --git a/messages/sdr.md b/messages/sdr.md index cb41b92705..cd5f752d18 100644 --- a/messages/sdr.md +++ b/messages/sdr.md @@ -157,3 +157,17 @@ No uniqueIdElement found in registry for %s (reading %s at %s). # uniqueIdElementNotInChild The uniqueIdElement %s was not found the child (reading %s at %s). + +# suggest_type_header + +A metadata type lookup for "%s" found the following close matches: + +# suggest_type_did_you_mean + +-- Did you mean ".%s%s" instead for the "%s" metadata type? + +# suggest_type_more_suggestions + +Additional suggestions: +Confirm the file name, extension, and directory names are correct. Validate against the registry at: +https://github.com/forcedotcom/source-deploy-retrieve/blob/main/src/registry/metadataRegistry.json diff --git a/package.json b/package.json index 9ac0aa4bc0..07cd9aa5d5 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@salesforce/kit": "^1.9.2", "@salesforce/ts-types": "^1.7.2", "archiver": "^5.3.1", + "fast-levenshtein": "^3.0.0", "fast-xml-parser": "^4.1.4", "got": "^11.8.6", "graceful-fs": "^4.2.11", @@ -47,6 +48,7 @@ "@salesforce/ts-sinon": "^1.4.6", "@types/archiver": "^5.3.1", "@types/deep-equal-in-any-order": "^1.0.1", + "@types/fast-levenshtein": "^0.0.2", "@types/graceful-fs": "^4.1.6", "@types/mime": "2.0.3", "@types/minimatch": "^5.1.2", @@ -101,6 +103,7 @@ "test": "wireit", "test:nuts": "mocha \"test/nuts/local/**/*.nut.ts\" --timeout 500000", "test:nuts:scale": "mocha \"test/nuts/scale/eda.nut.ts\" --timeout 500000; mocha \"test/nuts/scale/lotsOfClasses.nut.ts\" --timeout 500000; mocha \"test/nuts/scale/lotsOfClassesOneDir.nut.ts\" --timeout 500000", + "test:nuts:suggest": "mocha \"test/nuts/suggestType/suggestType.nut.ts\" --timeout 10000", "test:only": "wireit", "test:registry": "mocha ./test/registry/registryCompleteness.test.ts --timeout 50000", "update-registry": "npx ts-node scripts/update-registry/update2.ts", @@ -188,4 +191,4 @@ "output": [] } } -} \ No newline at end of file +} diff --git a/src/client/metadataTransfer.ts b/src/client/metadataTransfer.ts index 5b1d5f411d..d345685f83 100644 --- a/src/client/metadataTransfer.ts +++ b/src/client/metadataTransfer.ts @@ -162,13 +162,13 @@ export abstract class MetadataTransfer< } protected async maybeSaveTempDirectory(target: SfdxFileFormat, cs?: ComponentSet): Promise { - const mdapiTempDir = process.env.SFDX_MDAPI_TEMP_DIR; + const mdapiTempDir = process.env.SF_MDAPI_TEMP_DIR; if (mdapiTempDir) { await Lifecycle.getInstance().emitWarning( - 'The SFDX_MDAPI_TEMP_DIR environment variable is set, which may degrade performance' + 'The SF_MDAPI_TEMP_DIR environment variable is set, which may degrade performance' ); this.logger.debug( - `Converting metadata to: ${mdapiTempDir} because the SFDX_MDAPI_TEMP_DIR environment variable is set` + `Converting metadata to: ${mdapiTempDir} because the SF_MDAPI_TEMP_DIR environment variable is set` ); try { const source = cs ?? this.components ?? new ComponentSet(); diff --git a/src/registry/registryAccess.ts b/src/registry/registryAccess.ts index ce98853200..2aaf9762ea 100644 --- a/src/registry/registryAccess.ts +++ b/src/registry/registryAccess.ts @@ -5,6 +5,7 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import { Messages, SfError } from '@salesforce/core'; +import * as Levenshtein from 'fast-levenshtein'; import { registry as defaultRegistry } from './registry'; import { MetadataRegistry, MetadataType } from './types'; @@ -60,12 +61,44 @@ export class RegistryAccess { * @returns The corresponding metadata type object */ public getTypeBySuffix(suffix: string): MetadataType | undefined { - if (this.registry.suffixes?.[suffix]) { + if (this.registry.suffixes[suffix]) { const typeId = this.registry.suffixes[suffix]; return this.getTypeByName(typeId); } } + /** + * Find similar metadata type matches by its file suffix + * + * @param suffix - File suffix of the metadata type + * @returns An array of similar suffix and metadata type matches + */ + public guessTypeBySuffix( + suffix: string + ): Array<{ suffixGuess: string; metadataTypeGuess: MetadataType }> | undefined { + const registryKeys = Object.keys(this.registry.suffixes); + + const scores = registryKeys.map((registryKey) => ({ + registryKey, + score: Levenshtein.get(suffix, registryKey, { useCollator: true }), + })); + const sortedScores = scores.sort((a, b) => a.score - b.score); + const lowestScore = sortedScores[0].score; + // Levenshtein uses positive integers for scores, find all scores that match the lowest score + const guesses = sortedScores.filter((score) => score.score === lowestScore); + + if (guesses.length > 0) { + return guesses.map((guess) => { + const typeId = this.registry.suffixes[guess.registryKey]; + const metadataType = this.getTypeByName(typeId); + return { + suffixGuess: guess.registryKey, + metadataTypeGuess: metadataType, + }; + }); + } + } + /** * Searches for the first metadata type in the registry that returns `true` * for the given predicate function. diff --git a/src/registry/types.ts b/src/registry/types.ts index e7714946d5..506dede45d 100644 --- a/src/registry/types.ts +++ b/src/registry/types.ts @@ -11,7 +11,7 @@ */ export interface MetadataRegistry { types: TypeIndex; - suffixes?: SuffixIndex; + suffixes: SuffixIndex; strictDirectoryNames: { [directoryName: string]: string; }; diff --git a/src/resolve/metadataResolver.ts b/src/resolve/metadataResolver.ts index c41f3ab1e5..79c7db3ecd 100644 --- a/src/resolve/metadataResolver.ts +++ b/src/resolve/metadataResolver.ts @@ -5,7 +5,7 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import { basename, dirname, join, sep } from 'path'; -import { Lifecycle, Messages, SfError } from '@salesforce/core'; +import { Lifecycle, Messages, SfError, Logger } from '@salesforce/core'; import { extName, parentName, parseMetadataXml } from '../utils'; import { MetadataType, RegistryAccess } from '../registry'; import { ComponentSet } from '../collections'; @@ -25,6 +25,7 @@ const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sd */ export class MetadataResolver { public forceIgnoredPaths: Set; + protected logger: Logger; private forceIgnore?: ForceIgnore; private sourceAdapterFactory: SourceAdapterFactory; private folderContentTypeDirNames?: string[]; @@ -38,6 +39,7 @@ export class MetadataResolver { private tree: TreeContainer = new NodeFSTreeContainer(), private useFsForceIgnore = true ) { + this.logger = Logger.childFromRoot(this.constructor.name); this.sourceAdapterFactory = new SourceAdapterFactory(this.registry, tree); this.forceIgnoredPaths = new Set(); } @@ -139,13 +141,39 @@ export class MetadataResolver { !adapter.allowMetadataWithContent(); return shouldResolve ? adapter.getComponent(fsPath, isResolvingSource) : undefined; } + + // Perform some additional checks to see if this is a package manifest + if (fsPath.endsWith('.xml') && !fsPath.endsWith(META_XML_SUFFIX)) { + // If it is named the default package.xml, assume it is a package manifest + if (fsPath.endsWith('package.xml')) return undefined; + try { + // If the file contains the string " 0 + ? [ + messages.getMessage('suggest_type_header', [ basename(fsPath) ]), + ...guesses.map((guess) => + messages.getMessage('suggest_type_did_you_mean', [ + guess.suffixGuess, + (metaSuffix || closeMetaSuffix) ? '-meta.xml' : '', + guess.metadataTypeGuess.name, + ]) + ), + '', // A blank line makes this much easier to read (it doesn't seem to be possible to start a markdown message entry with a newline) + messages.getMessage('suggest_type_more_suggestions'), + ] + : []; + } + /** * Whether or not a directory that represents a single component should be resolved as one, * or if it should be walked for additional components. diff --git a/test/client/metadataApiDeploy.test.ts b/test/client/metadataApiDeploy.test.ts index 48cef7a7a2..fe2d4d677a 100644 --- a/test/client/metadataApiDeploy.test.ts +++ b/test/client/metadataApiDeploy.test.ts @@ -82,7 +82,7 @@ describe('MetadataApiDeploy', () => { it('should save the temp directory if the environment variable is set', async () => { try { - process.env.SFDX_MDAPI_TEMP_DIR = 'test'; + process.env.SF_MDAPI_TEMP_DIR = 'test'; const components = new ComponentSet([matchingContentFile.COMPONENT]); const { operation, convertStub, deployStub } = await stubMetadataDeploy($$, testOrg, { components, @@ -95,7 +95,7 @@ describe('MetadataApiDeploy', () => { expect(deployStub.firstCall.args[0]).to.equal(zipBuffer); expect(getString(convertStub.secondCall.args[2], 'outputDirectory', '')).to.equal('test'); } finally { - delete process.env.SFDX_MDAPI_TEMP_DIR; + delete process.env.SF_MDAPI_TEMP_DIR; } }); diff --git a/test/client/metadataApiRetrieve.test.ts b/test/client/metadataApiRetrieve.test.ts index 6ea404bce6..4d544a9a03 100644 --- a/test/client/metadataApiRetrieve.test.ts +++ b/test/client/metadataApiRetrieve.test.ts @@ -303,7 +303,7 @@ describe('MetadataApiRetrieve', () => { it('should save the temp directory if the environment variable is set', async () => { try { - process.env.SFDX_MDAPI_TEMP_DIR = 'test'; + process.env.SF_MDAPI_TEMP_DIR = 'test'; const toRetrieve = new ComponentSet([COMPONENT]); const { operation, convertStub } = await stubMetadataRetrieve($$, testOrg, { toRetrieve, @@ -317,7 +317,7 @@ describe('MetadataApiRetrieve', () => { expect(getString(convertStub.secondCall.args[2], 'outputDirectory', '')).to.equal('test'); } finally { - delete process.env.SFDX_MDAPI_TEMP_DIR; + delete process.env.SF_MDAPI_TEMP_DIR; } }); diff --git a/test/nuts/suggestType/suggestType.nut.ts b/test/nuts/suggestType/suggestType.nut.ts new file mode 100644 index 0000000000..a941764e26 --- /dev/null +++ b/test/nuts/suggestType/suggestType.nut.ts @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2020, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import * as path from 'path'; +import { TestSession } from '@salesforce/cli-plugins-testkit'; +import { expect, config } from 'chai'; +import { SfError, Messages } from '@salesforce/core'; +import { ComponentSetBuilder } from '../../../src'; + +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr'); + +config.truncateThreshold = 0; + +describe('suggest types', () => { + let session: TestSession; + + before(async () => { + session = await TestSession.create({ + project: { + sourceDir: path.join('test', 'nuts', 'suggestType', 'testProj'), + }, + devhubAuthStrategy: 'NONE', + }); + }); + + after(async () => { + await session?.clean(); + }); + + it('it offers a suggestions on an invalid metadata suffix', async () => { + try { + await ComponentSetBuilder.build({ + sourcepath: [path.join(session.project.dir, 'force-app', 'main', 'default', 'objects')], + }); + throw new Error('This test should have thrown'); + } catch (err) { + const error = err as SfError; + expect(error.name).to.equal('TypeInferenceError'); + expect(error.actions).to.include( + 'A metadata type lookup for "MyTestObject__c.objct-meta.xml" found the following close matches:' + ); + expect(error.actions).to.include( + '-- Did you mean ".object-meta.xml" instead for the "CustomObject" metadata type?' + ); + } + }); + + it('it offers a suggestions on an invalid filename suffix', async () => { + try { + await ComponentSetBuilder.build({ + sourcepath: [path.join(session.project.dir, 'force-app', 'main', 'default', 'classes', 'DummyClass.clss')], + }); + throw new Error('This test should have thrown'); + } catch (err) { + const error = err as SfError; + expect(error.name).to.equal('TypeInferenceError'); + expect(error.actions).to.include('A metadata type lookup for "DummyClass.clss" found the following close matches:'); + expect(error.actions).to.include('-- Did you mean ".cls" instead for the "ApexClass" metadata type?'); + } + }); + + it('it offers a suggestions on a incorrect casing', async () => { + try { + await ComponentSetBuilder.build({ + sourcepath: [path.join(session.project.dir, 'force-app', 'main', 'default', 'layouts')], + }); + throw new Error('This test should have thrown'); + } catch (err) { + const error = err as SfError; + expect(error.name).to.equal('TypeInferenceError'); + expect(error.actions).to.include( + 'A metadata type lookup for "MyTestObject__c-MyTestObject Layout.Layout-meta.xml" found the following close matches:' + ); + expect(error.actions).to.include('-- Did you mean ".layout-meta.xml" instead for the "Layout" metadata type?'); + } + }); + + it('it offers multiple suggestions if Levenshtein distance is the same', async () => { + try { + await ComponentSetBuilder.build({ + sourcepath: [path.join(session.project.dir, 'force-app', 'main', 'default', 'tabs')], + }); + throw new Error('This test should have thrown'); + } catch (err) { + const error = err as SfError; + expect(error.name).to.equal('TypeInferenceError'); + expect(error.actions).to.include( + 'A metadata type lookup for "Settings.tabsss-meta.xml" found the following close matches:' + ); + expect(error.actions).to.include( + '-- Did you mean ".labels-meta.xml" instead for the "CustomLabels" metadata type?' + ); + expect(error.actions).to.include('-- Did you mean ".tab-meta.xml" instead for the "CustomTab" metadata type?'); + } + }); + + it('it offers additional suggestions to try', async () => { + try { + await ComponentSetBuilder.build({ + sourcepath: [path.join(session.project.dir, 'force-app', 'main', 'default', 'tabs')], + }); + throw new Error('This test should have thrown'); + } catch (err) { + const error = err as SfError; + expect(error.name).to.equal('TypeInferenceError'); + expect(error.actions).to.include(messages.getMessage('suggest_type_more_suggestions')); + } + }); + + // Since EmailServicesFunction uses the 'xml' suffix, we want to ensure it still resolves correctly + it('it still correctly resolves an EmailServicesFunction', async () => { + const cs = await ComponentSetBuilder.build({ + sourcepath: [path.join(session.project.dir, 'force-app', 'main', 'default', 'emailservices')], + }); + expect(cs['components'].size).to.equal(1); + expect((await cs.getObject()).Package.types[0].name).to.equal('EmailServicesFunction'); + }); + + // This uses the closeMetaSuffix lookup + it('it errors on very incorrectly named metadata', async () => { + try { + await ComponentSetBuilder.build({ + sourcepath: [path.join(session.project.dir, 'force-app', 'main', 'default', 'labels')], + }); + throw new Error('This test should have thrown'); + } catch (err) { + const error = err as SfError; + expect(error.name).to.equal('TypeInferenceError'); + expect(error.actions).to.include('A metadata type lookup for "CustomLabels.labels.xml" found the following close matches:'); + expect(error.actions).to.include('-- Did you mean ".labels-meta.xml" instead for the "CustomLabels" metadata type?'); + } + }); + + it('it ignores package manifest files with default name', async () => { + const cs = await ComponentSetBuilder.build({ sourcepath: [path.join(session.project.dir, 'package-manifest')] }); + expect(cs['components'].size).to.equal(0); + }); + + it('it ignores package manifest files with non-default name', async () => { + const cs = await ComponentSetBuilder.build({ sourcepath: [path.join(session.project.dir, 'package-manifest-2')] }); + expect(cs['components'].size).to.equal(0); + }); +}); diff --git a/test/nuts/suggestType/testProj/config/project-scratch-def.json b/test/nuts/suggestType/testProj/config/project-scratch-def.json new file mode 100644 index 0000000000..c8b8be34c3 --- /dev/null +++ b/test/nuts/suggestType/testProj/config/project-scratch-def.json @@ -0,0 +1,13 @@ +{ + "orgName": "ewillhoit company", + "edition": "Developer", + "features": ["EnableSetPasswordInApi"], + "settings": { + "lightningExperienceSettings": { + "enableS1DesktopEnabled": true + }, + "mobileSettings": { + "enableS1EncryptedStoragePref2": false + } + } +} diff --git a/test/nuts/suggestType/testProj/force-app/main/default/classes/DummyClass.clss b/test/nuts/suggestType/testProj/force-app/main/default/classes/DummyClass.clss new file mode 100644 index 0000000000..4e198f5059 --- /dev/null +++ b/test/nuts/suggestType/testProj/force-app/main/default/classes/DummyClass.clss @@ -0,0 +1,4 @@ +global class DummyClass { + result.success = true; + return result; +} diff --git a/test/nuts/suggestType/testProj/force-app/main/default/classes/MyEmailHandler.cls b/test/nuts/suggestType/testProj/force-app/main/default/classes/MyEmailHandler.cls new file mode 100644 index 0000000000..e5bf4b950b --- /dev/null +++ b/test/nuts/suggestType/testProj/force-app/main/default/classes/MyEmailHandler.cls @@ -0,0 +1,10 @@ +global class MyEmailHandler implements Messaging.InboundEmailHandler { + global Messaging.InboundEmailResult handleInboundEmail(Messaging.InboundEmail email, Messaging.InboundEnvelope envelope) { + Messaging.InboundEmailResult result = new Messaging.InboundEmailresult(); + + // logic here + + result.success = true; + return result; + } +} diff --git a/test/nuts/suggestType/testProj/force-app/main/default/classes/MyEmailHandler.cls-meta.xml b/test/nuts/suggestType/testProj/force-app/main/default/classes/MyEmailHandler.cls-meta.xml new file mode 100644 index 0000000000..c8d39860d4 --- /dev/null +++ b/test/nuts/suggestType/testProj/force-app/main/default/classes/MyEmailHandler.cls-meta.xml @@ -0,0 +1,6 @@ + + + 57.0 + Active + MyEmailHandler.clz + diff --git a/test/nuts/suggestType/testProj/force-app/main/default/emailservices/EmailServiceName.xml-meta.xml b/test/nuts/suggestType/testProj/force-app/main/default/emailservices/EmailServiceName.xml-meta.xml new file mode 100644 index 0000000000..c7db5c3a0e --- /dev/null +++ b/test/nuts/suggestType/testProj/force-app/main/default/emailservices/EmailServiceName.xml-meta.xml @@ -0,0 +1,22 @@ + + + MyEmailHandler + None + Discard + Discard + + alm-cli@salesforce.com + MyEmailAddress + true + emailservicename + test-x4oy7orqzey1@example.com + + Discard + EmailServiceName + false + false + false + false + false + Discard + diff --git a/test/nuts/suggestType/testProj/force-app/main/default/labels/CustomLabels.labels.xml b/test/nuts/suggestType/testProj/force-app/main/default/labels/CustomLabels.labels.xml new file mode 100644 index 0000000000..552fe7303c --- /dev/null +++ b/test/nuts/suggestType/testProj/force-app/main/default/labels/CustomLabels.labels.xml @@ -0,0 +1,10 @@ + + + + MyCustomLabel + en_US + true + MyCustomLabel + foo + + diff --git a/test/nuts/suggestType/testProj/force-app/main/default/layouts/MyTestObject__c-MyTestObject Layout.Layout-meta.xml b/test/nuts/suggestType/testProj/force-app/main/default/layouts/MyTestObject__c-MyTestObject Layout.Layout-meta.xml new file mode 100644 index 0000000000..a4c98b2f54 --- /dev/null +++ b/test/nuts/suggestType/testProj/force-app/main/default/layouts/MyTestObject__c-MyTestObject Layout.Layout-meta.xml @@ -0,0 +1,55 @@ + + + + false + false + true + + + + Required + Name + + + + + Edit + OwnerId + + + + + + false + false + true + + + + Readonly + CreatedById + + + + + Readonly + LastModifiedById + + + + + + false + false + true + + + + + + false + false + false + false + false + diff --git a/test/nuts/suggestType/testProj/force-app/main/default/objects/MyTestObject__c/MyTestObject__c.objct-meta.xml b/test/nuts/suggestType/testProj/force-app/main/default/objects/MyTestObject__c/MyTestObject__c.objct-meta.xml new file mode 100644 index 0000000000..28db778254 --- /dev/null +++ b/test/nuts/suggestType/testProj/force-app/main/default/objects/MyTestObject__c/MyTestObject__c.objct-meta.xml @@ -0,0 +1,165 @@ + + + + Accept + Default + + + Accept + Large + Default + + + Accept + Small + Default + + + CancelEdit + Default + + + CancelEdit + Large + Default + + + CancelEdit + Small + Default + + + Clone + Default + + + Clone + Large + Default + + + Clone + Small + Default + + + Delete + Default + + + Delete + Large + Default + + + Delete + Small + Default + + + Edit + Default + + + Edit + Large + Default + + + Edit + Small + Default + + + List + Default + + + List + Large + Default + + + List + Small + Default + + + New + Default + + + New + Large + Default + + + New + Small + Default + + + SaveEdit + Default + + + SaveEdit + Large + Default + + + SaveEdit + Small + Default + + + Tab + Default + + + Tab + Large + Default + + + Tab + Small + Default + + + View + Default + + + View + Large + Default + + + View + Small + Default + + false + SYSTEM + Deployed + false + true + false + false + false + false + false + true + true + Private + + + + Text + + MyTestObject + + ReadWrite + Public + diff --git a/test/nuts/suggestType/testProj/force-app/main/default/tabs/Settings.tabsss-meta.xml b/test/nuts/suggestType/testProj/force-app/main/default/tabs/Settings.tabsss-meta.xml new file mode 100644 index 0000000000..e93d3c8faf --- /dev/null +++ b/test/nuts/suggestType/testProj/force-app/main/default/tabs/Settings.tabsss-meta.xml @@ -0,0 +1,7 @@ + + + Created by Lightning App Builder + Settings + + Custom19: Wrench + diff --git a/test/nuts/suggestType/testProj/package-manifest-2/package-non-default.xml b/test/nuts/suggestType/testProj/package-manifest-2/package-non-default.xml new file mode 100644 index 0000000000..e4f345f282 --- /dev/null +++ b/test/nuts/suggestType/testProj/package-manifest-2/package-non-default.xml @@ -0,0 +1,8 @@ + + + + label_one + CustomLabel + + 55.0 + \ No newline at end of file diff --git a/test/nuts/suggestType/testProj/package-manifest/package.xml b/test/nuts/suggestType/testProj/package-manifest/package.xml new file mode 100644 index 0000000000..e4f345f282 --- /dev/null +++ b/test/nuts/suggestType/testProj/package-manifest/package.xml @@ -0,0 +1,8 @@ + + + + label_one + CustomLabel + + 55.0 + \ No newline at end of file diff --git a/test/nuts/suggestType/testProj/sfdx-project.json b/test/nuts/suggestType/testProj/sfdx-project.json new file mode 100644 index 0000000000..0cb02e4e82 --- /dev/null +++ b/test/nuts/suggestType/testProj/sfdx-project.json @@ -0,0 +1,12 @@ +{ + "packageDirectories": [ + { + "path": "force-app", + "default": true + } + ], + "name": "testProj", + "namespace": "", + "sfdcLoginUrl": "https://login.salesforce.com", + "sourceApiVersion": "57.0" +} diff --git a/test/registry/registryAccess.test.ts b/test/registry/registryAccess.test.ts index e639027dd7..6d585e87b7 100644 --- a/test/registry/registryAccess.test.ts +++ b/test/registry/registryAccess.test.ts @@ -110,4 +110,15 @@ describe('RegistryAccess', () => { expect(registryAccess.getParentType(registry.types.digitalexperienceconfig.id)).to.be.undefined; }); }); + + describe('suggestions', () => { + it('guess for a type that is all uppercase should return the correct type first', () => { + const result = registryAccess.guessTypeBySuffix('CLS'); + expect(result?.[0].metadataTypeGuess.name).to.equal('ApexClass'); + }); + it('guess for a type that is first-uppercase should return the correct type first', () => { + const result = registryAccess.guessTypeBySuffix('Cls'); + expect(result?.[0].metadataTypeGuess.name).to.equal('ApexClass'); + }); + }); }); diff --git a/yarn.lock b/yarn.lock index babac0c549..8043147097 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1124,6 +1124,11 @@ resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5" integrity sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg== +"@types/fast-levenshtein@^0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@types/fast-levenshtein/-/fast-levenshtein-0.0.2.tgz#9f618cff4469da8df46c9ee91b1c95b9af1d8f6a" + integrity sha512-h9AGeNlFimLtFUlEZgk+hb3LUT4tNHu8y0jzCUeTdi1BM4e86sBQs/nQYgHk70ksNyNbuLwpymFAXkb0GAehmw== + "@types/glob@~7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"