@vlocode/apex
Version:
Salesforce APEX Parser and Grammar
176 lines • 8.1 kB
JavaScript
;
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TestIdentifier = void 0;
const core_1 = require("@vlocode/core");
const util_1 = require("@vlocode/util");
const path_1 = __importDefault(require("path"));
const parser_1 = require("./parser");
/**
* This class can be used to identify test classes that cover a given Apex class, it
* does so by parsing the source files and identifying test classes.
*
* The class is injectable and depends on the `FileSystem` and `Logger` services. These services can be injected
* by the dependency injection container see {@link container}
*
* @example
* ```typescript
* const testIdentifier = container.create(TestIdentifier);
* await testIdentifier.loadApexClasses([ 'path/to/apex/classes' ]);
* const testClasses = testIdentifier.getTestClasses('MyClass');
* ```
*/
let TestIdentifier = class TestIdentifier {
fileSystem;
logger;
/**
* Full path of the file to the apex class name.
*/
fileToApexClass = new Map();
/**
* Map of Apex class information by name class name (lowercase)
*/
apexClassesByName = new Map();
/**
* Set of test class names
*/
testClasses = new Map();
constructor(fileSystem, logger = core_1.LogManager.get('apex-test-identifier')) {
this.fileSystem = fileSystem;
this.logger = logger;
}
/**
* Loads the Apex classes from the specified folders and populates the testClasses map.
*
* @param folders - An array of folder paths containing the Apex classes.
* @returns A promise that resolves when the Apex classes are loaded and testClasses map is populated.
*/
async loadApexClasses(folders) {
const timerAll = new util_1.Timer();
const loadedFiles = await this.parseSourceFiles(folders);
const testClasses = loadedFiles.filter((info) => info.classStructure.methods.some(method => method.isTest));
testClasses.forEach(testClass => this.testClasses.set(testClass.name.toLowerCase(), {
name: testClass.name,
file: testClass.file,
classCoverage: testClass.classStructure.methods
.filter(method => method.isTest)
.flatMap(method => method.refs.filter(ref => !ref.isSystemType))
.map(ref => ref.name.toLowerCase()),
testMethods: testClass.classStructure.methods
.filter(method => method.isTest)
.map(method => ({
methodName: method.name,
classCoverage: method.refs.filter(ref => !ref.isSystemType).map(ref => ref.name.toLowerCase())
})),
}));
this.logger.info(`Loaded ${loadedFiles.length} sources (${testClasses.length} test classes) in ${timerAll.toString('ms')}`);
}
/**
* Retrieves the test classes that cover a given class, optionally include test classes for classes that reference the given class.
* The depth parameter controls how many levels of references to include, if not specified only direct test classes are returned.
*
* If the class is not found, undefined is returned; if no test classes are found, an empty array is returned.
*
* @param className - The name of the class to retrieve test classes for.
* @param options - Optional parameters for controlling the depth of the search.
* @returns An array of test class names that cover the specified class.
*/
getTestClasses(className, options) {
const classInfo = this.apexClassesByName.get(className.toLowerCase());
if (!classInfo) {
return undefined;
}
const testClasses = new Set();
for (const testClass of this.testClasses.values()) {
if (testClass.classCoverage.includes(className.toLowerCase())) {
testClasses.add(testClass.name);
}
}
if (options?.depth) {
for (const referenceClassName of this.getClassReferences(className)) {
this.getTestClasses(referenceClassName, { depth: options.depth - 1 })
?.forEach(testClass => testClasses.add(testClass));
}
}
return [...testClasses];
}
/**
* Retrieves an Array class that references the given class.
* @param className The name of the class to retrieve references for.
* @returns An array of class names that reference the given class.
*/
getClassReferences(className) {
const references = new Set();
for (const classInfo of this.apexClassesByName.values()) {
if (classInfo.classStructure.refs.some(ref => (0, util_1.stringEqualsIgnoreCase)(ref.name, className))) {
references.add(classInfo.name);
}
}
return [...references];
}
async parseSourceFiles(folders) {
const sourceFiles = new Array();
for (const folder of folders) {
this.logger.verbose(`Parsing source files in: ${folder}`);
for await (const { buffer, file } of this.readSourceFiles(folder)) {
const apexClassName = this.fileToApexClass.get(file);
if (apexClassName) {
sourceFiles.push(this.apexClassesByName.get(apexClassName.toLowerCase()));
continue;
}
const parseTimer = new util_1.Timer();
const parser = new parser_1.Parser(buffer);
const struct = parser.getCodeStructure();
for (const classInfo of struct.classes) {
const sourceData = {
classStructure: classInfo,
name: classInfo.name,
file,
isAbstract: !!classInfo.isAbstract,
isTest: !!classInfo.isTest,
};
this.fileToApexClass.set(file, classInfo.name);
this.apexClassesByName.set(classInfo.name.toLowerCase(), sourceData);
sourceFiles.push(sourceData);
}
this.logger.verbose(`Parsed: ${file} (${parseTimer.toString('ms')})`);
}
}
return sourceFiles;
}
async *readSourceFiles(folder) {
for (const file of await this.fileSystem.readDirectory(folder)) {
const fullPath = path_1.default.join(folder, file.name);
if (file.isDirectory()) {
yield* this.readSourceFiles(fullPath);
}
if (file.isFile() && file.name.endsWith('.cls') /* || file.name.endsWith('.trigger') */) {
yield {
buffer: await this.fileSystem.readFile(fullPath),
fullPath,
file: file.name
};
}
}
}
};
exports.TestIdentifier = TestIdentifier;
exports.TestIdentifier = TestIdentifier = __decorate([
(0, core_1.injectable)({ lifecycle: core_1.LifecyclePolicy.transient }),
__metadata("design:typeinfo", {
paramTypes: () => [core_1.FileSystem,
core_1.Logger]
})
], TestIdentifier);
//# sourceMappingURL=testIdentifier.js.map