diff --git a/packages/core/src/parser/ApexTypeFetcher.ts b/packages/core/src/parser/ApexTypeFetcher.ts new file mode 100644 index 000000000..35fd82da7 --- /dev/null +++ b/packages/core/src/parser/ApexTypeFetcher.ts @@ -0,0 +1,140 @@ +import fs from "fs-extra"; +const path = require("path"); +const glob = require("glob"); + +import { CommonTokenStream, ANTLRInputStream } from 'antlr4ts'; +import { ParseTreeWalker } from "antlr4ts/tree/ParseTreeWalker"; + +import ApexTypeListener from "./listeners/ApexTypeListener"; + +import { + ApexLexer, + ApexParser, + ApexParserListener, + ThrowingErrorListener +} from "apex-parser"; + +export default class ApexTypeFetcher { + + + /** + * Get Apex type of cls files in a search directory. + * Sorts files into classes, test classes and interfaces. + * @param searchDir + */ + public getApexTypeOfClsFiles(searchDir: string): ApexSortedByType { + const apexSortedByType: ApexSortedByType = { + class: [], + testClass: [], + interface: [], + parseError: [] + }; + + let clsFiles: string[]; + if (fs.existsSync(searchDir)) { + clsFiles = glob.sync(`*.cls`, { + cwd: searchDir, + absolute: true + }); + } else { + throw new Error(`Search directory ${searchDir} does not exist`); + } + + + for (let clsFile of clsFiles) { + + let clsPayload: string = fs.readFileSync(clsFile, 'utf8'); + let fileDescriptor: FileDescriptor = {name: path.basename(clsFile, ".cls"), filepath: clsFile}; + + // Parse cls file + let compilationUnitContext; + try { + let lexer = new ApexLexer(new ANTLRInputStream(clsPayload)); + let tokens: CommonTokenStream = new CommonTokenStream(lexer); + + let parser = new ApexParser(tokens); + parser.removeErrorListeners() + parser.addErrorListener(new ThrowingErrorListener()); + + compilationUnitContext = parser.compilationUnit(); + + } catch (err) { + console.log(`Failed to parse ${clsFile}`); + console.log(err); + + // Manually parse class if error is caused by System.runAs() or testMethod modifier + if ( + this.parseSystemRunAs(err, clsPayload) || + this.parseTestMethod(err, clsPayload) + ) { + console.log(`Manually identified test class ${clsFile}`) + apexSortedByType["testClass"].push(fileDescriptor); + } else { + fileDescriptor["error"] = err; + apexSortedByType["parseError"].push(fileDescriptor); + } + continue; + } + + let apexTypeListener: ApexTypeListener = new ApexTypeListener(); + + // Walk parse tree to determine Apex type + ParseTreeWalker.DEFAULT.walk(apexTypeListener as ApexParserListener, compilationUnitContext); + + let apexType = apexTypeListener.getApexType(); + + if (apexType.class) { + apexSortedByType["class"].push(fileDescriptor); + if (apexType.testClass) { + apexSortedByType["testClass"].push(fileDescriptor); + } + } else if (apexType.interface) { + apexSortedByType["interface"].push(fileDescriptor); + } else { + fileDescriptor["error"] = {message: "Unknown Apex Type"}; + apexSortedByType["parseError"].push(fileDescriptor); + } + } + + return apexSortedByType; + } + + /** + * Bypass error parsing System.runAs() + * @param error + * @param clsPayload + */ + private parseSystemRunAs(error, clsPayload: string): boolean { + return ( + error["message"].includes("missing ';' at '{'") && + /System.runAs/i.test(clsPayload) && + /@isTest/i.test(clsPayload) + ); + } + + /** + * Bypass error parsing testMethod modifier + * @param error + * @param clsPayload + */ + private parseTestMethod(error, clsPayload: string): boolean { + return ( + error["message"].includes("no viable alternative at input") && + /testMethod/i.test(error["message"]) && + /testMethod/i.test(clsPayload) + ); + } +} + +interface ApexSortedByType { + class: FileDescriptor[], + testClass: FileDescriptor[], + interface: FileDescriptor[], + parseError: FileDescriptor[] +} + +interface FileDescriptor { + name: string + filepath: string, + error?: any +} diff --git a/packages/core/src/parser/InterfaceFetcher.ts b/packages/core/src/parser/InterfaceFetcher.ts deleted file mode 100644 index b32909813..000000000 --- a/packages/core/src/parser/InterfaceFetcher.ts +++ /dev/null @@ -1,76 +0,0 @@ -import fs from "fs-extra"; -const path = require("path"); -const glob = require("glob"); - -import { CommonTokenStream, ANTLRInputStream } from 'antlr4ts'; -import { ParseTreeWalker } from "antlr4ts/tree/ParseTreeWalker"; - -import InterfaceDeclarationListener from "./listeners/InterfaceDeclarationListener"; - -import { - ApexLexer, - ApexParser, - ApexParserListener, - ThrowingErrorListener -} from "apex-parser"; - -export default class InterfaceFetcher { - public unparsedClasses: string[]; - - constructor() { - this.unparsedClasses = []; - } - - /** - * Get name of interfaces in a search directory. - * An empty array is returned if no interfaces are found. - * @param searchDir - */ - public getInterfaceNames(searchDir: string): string[] { - const interfaceNames: string[] = []; - - let clsFiles: string[]; - if (fs.existsSync(searchDir)) { - clsFiles = glob.sync(`*.cls`, { - cwd: searchDir, - absolute: true - }); - } else { - throw new Error(`Search directory ${searchDir} does not exist`); - } - - for (let clsFile of clsFiles) { - - let clsPayload: string = fs.readFileSync(clsFile, 'utf8'); - - let compilationUnitContext; - try { - let lexer = new ApexLexer(new ANTLRInputStream(clsPayload)); - let tokens: CommonTokenStream = new CommonTokenStream(lexer); - - let parser = new ApexParser(tokens); - parser.removeErrorListeners() - parser.addErrorListener(new ThrowingErrorListener()); - - compilationUnitContext = parser.compilationUnit(); - - } catch (err) { - console.log(`Failed to parse ${clsFile}`); - console.log(err); - this.unparsedClasses.push(path.basename(clsFile, ".cls")); - continue; - } - - let interfaceDeclarationListener: InterfaceDeclarationListener = new InterfaceDeclarationListener(); - - ParseTreeWalker.DEFAULT.walk(interfaceDeclarationListener as ApexParserListener, compilationUnitContext); - - if (interfaceDeclarationListener.getInterfaceDeclarationCount() > 0) { - let className: string = path.basename(clsFile, ".cls"); - interfaceNames.push(className); - } - } - - return interfaceNames; - } -} diff --git a/packages/core/src/parser/TestClassFetcher.ts b/packages/core/src/parser/TestClassFetcher.ts deleted file mode 100644 index 973983fad..000000000 --- a/packages/core/src/parser/TestClassFetcher.ts +++ /dev/null @@ -1,76 +0,0 @@ -import fs from "fs-extra"; -const path = require("path"); -const glob = require("glob"); - -import { CommonTokenStream, ANTLRInputStream } from 'antlr4ts'; -import { ParseTreeWalker } from "antlr4ts/tree/ParseTreeWalker"; - -import TestAnnotationListener from "./listeners/TestAnnotationListener"; - -import { - ApexLexer, - ApexParser, - ApexParserListener, - ThrowingErrorListener -} from "apex-parser"; - -export default class TestClassFetcher { - public unparsedClasses: string[]; - - constructor() { - this.unparsedClasses = []; - } - - /** - * Get name of test classes in a search directory. - * An empty array is returned if no test classes are found. - * @param searchDir - */ - public getTestClassNames(searchDir: string): string[] { - const testClassNames: string[] = []; - - let clsFiles: string[]; - if (fs.existsSync(searchDir)) { - clsFiles = glob.sync(`*.cls`, { - cwd: searchDir, - absolute: true - }); - } else { - throw new Error(`Search directory ${searchDir} does not exist`); - } - - for (let clsFile of clsFiles) { - - let clsPayload: string = fs.readFileSync(clsFile, 'utf8'); - - let compilationUnitContext; - try { - let lexer = new ApexLexer(new ANTLRInputStream(clsPayload)); - let tokens: CommonTokenStream = new CommonTokenStream(lexer); - - let parser = new ApexParser(tokens); - parser.removeErrorListeners() - parser.addErrorListener(new ThrowingErrorListener()); - - compilationUnitContext = parser.compilationUnit(); - - } catch (err) { - console.log(`Failed to parse ${clsFile}`); - console.log(err); - this.unparsedClasses.push(path.basename(clsFile, ".cls")); - continue; - } - - let testAnnotationListener: TestAnnotationListener = new TestAnnotationListener(); - - ParseTreeWalker.DEFAULT.walk(testAnnotationListener as ApexParserListener, compilationUnitContext); - - if (testAnnotationListener.getTestAnnotationCount() > 0) { - let className: string = path.basename(clsFile, ".cls"); - testClassNames.push(className); - } - } - - return testClassNames; - } -} diff --git a/packages/core/src/parser/listeners/AnnotationListener.ts b/packages/core/src/parser/listeners/AnnotationListener.ts deleted file mode 100644 index d364bce07..000000000 --- a/packages/core/src/parser/listeners/AnnotationListener.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApexParserListener, AnnotationContext } from "apex-parser"; - -export default class AnnotationListener implements ApexParserListener { - private annotationCount: number = 0; - - protected enterAnnotation(ctx: AnnotationContext) { - this.annotationCount += 1; - } - - private exitAnnotation(ctx: AnnotationContext) { - // Perform some logic - } - - public getAnnotationCount(): number { - return this.annotationCount; - } -} diff --git a/packages/core/src/parser/listeners/ApexTypeListener.ts b/packages/core/src/parser/listeners/ApexTypeListener.ts new file mode 100644 index 000000000..aa3734a87 --- /dev/null +++ b/packages/core/src/parser/listeners/ApexTypeListener.ts @@ -0,0 +1,39 @@ +import { + ApexParserListener, + AnnotationContext, + InterfaceDeclarationContext, + ClassDeclarationContext +} from "apex-parser"; + + +export default class ApexTypeListener implements ApexParserListener{ + private apexType: ApexType = { + class: false, + testClass: false, + interface: false + } + + protected enterAnnotation(ctx: AnnotationContext): void { + if (ctx._stop.text.toUpperCase() === "ISTEST") { + this.apexType["testClass"] = true; + } + } + + private enterInterfaceDeclaration(ctx: InterfaceDeclarationContext): void { + this.apexType["interface"] = true; + } + + private enterClassDeclaration(ctx: ClassDeclarationContext): void { + this.apexType["class"] = true; + } + + public getApexType(): ApexType { + return this.apexType; + } +} + +interface ApexType { + class: boolean, + testClass: boolean, + interface: boolean +} diff --git a/packages/core/src/parser/listeners/InterfaceDeclarationListener.ts b/packages/core/src/parser/listeners/InterfaceDeclarationListener.ts deleted file mode 100644 index 8368db551..000000000 --- a/packages/core/src/parser/listeners/InterfaceDeclarationListener.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ApexParserListener, InterfaceDeclarationContext } from "apex-parser"; - -export default class InterfaceDeclarationListener - implements ApexParserListener { - private interfaceDeclarationCount: number = 0; - - private enterInterfaceDeclaration(ctx: InterfaceDeclarationContext) { - this.interfaceDeclarationCount += 1; - } - - private exitInterfaceDeclaration(ctx: InterfaceDeclarationContext) { - // Perform some logic - } - - public getInterfaceDeclarationCount(): number { - return this.interfaceDeclarationCount; - } -} diff --git a/packages/core/src/parser/listeners/TestAnnotationListener.ts b/packages/core/src/parser/listeners/TestAnnotationListener.ts deleted file mode 100644 index b4a796a9c..000000000 --- a/packages/core/src/parser/listeners/TestAnnotationListener.ts +++ /dev/null @@ -1,17 +0,0 @@ -import AnnotationListener from "./AnnotationListener"; -import { AnnotationContext } from "apex-parser"; - -export default class TestAnnotationListener extends AnnotationListener { - private testAnnotationCount: number = 0; - - protected enterAnnotation(ctx: AnnotationContext) { - super.enterAnnotation(ctx); - if (ctx._stop.text.toUpperCase() === "ISTEST") { - this.testAnnotationCount += 1; - } - } - - public getTestAnnotationCount(): number { - return this.testAnnotationCount; - } -} diff --git a/packages/core/src/sfdxwrappers/TriggerApexTestImpl.ts b/packages/core/src/sfdxwrappers/TriggerApexTestImpl.ts index 70a3da53c..e6c196b06 100644 --- a/packages/core/src/sfdxwrappers/TriggerApexTestImpl.ts +++ b/packages/core/src/sfdxwrappers/TriggerApexTestImpl.ts @@ -4,8 +4,7 @@ import { isNullOrUndefined } from "util"; import fs = require("fs-extra"); import path = require("path"); import MDAPIPackageGenerator from "../generators/MDAPIPackageGenerator"; -import TestClassFetcher from "../parser/TestClassFetcher"; -import InterfaceFetcher from "../parser/InterfaceFetcher"; +import ApexTypeFetcher from "../parser/ApexTypeFetcher"; import ManifestHelpers from "../manifest/ManifestHelpers"; export default class TriggerApexTestImpl { @@ -277,23 +276,22 @@ export default class TriggerApexTestImpl { } if (packageClasses != null) { - // Remove test classes from package classes - // if (fs.existsSync(path.join(mdapiPackage.mdapiDir, `classes`))) - let testClassFetcher: TestClassFetcher = new TestClassFetcher(); - let testClasses: string[] = testClassFetcher.getTestClassNames(path.join(mdapiPackage.mdapiDir, `classes`)); - if (testClasses.length > 0) { + let apexTypeFetcher: ApexTypeFetcher = new ApexTypeFetcher(); + let apexSortedByType = apexTypeFetcher.getApexTypeOfClsFiles(path.join(mdapiPackage.mdapiDir, `classes`)); + + if (apexSortedByType["testClass"].length > 0) { // Filter out test classes packageClasses = packageClasses.filter( (packageClass) => { - for (let testClass of testClasses) { - if (testClass === packageClass) { + for (let testClass of apexSortedByType["testClass"]) { + if (testClass["name"] === packageClass) { return false; } } - if (testClassFetcher.unparsedClasses.length > 0) { + if (apexSortedByType["parseError"].length > 0) { // Filter out undetermined classes that failed to parse - for (let unparsedClass of testClassFetcher.unparsedClasses) { - if (unparsedClass === packageClass) { + for (let parseError of apexSortedByType["parseError"]) { + if (parseError["name"] === packageClass) { console.log(`Skipping coverage validation for ${packageClass}, unable to determine identity of class`); return false; } @@ -304,18 +302,14 @@ export default class TriggerApexTestImpl { }); } - // Remove interfaces from package classes - let interfaceFetcher: InterfaceFetcher = new InterfaceFetcher(); - let interfaceNames: string[] = interfaceFetcher.getInterfaceNames(path.join(mdapiPackage.mdapiDir, `classes`)); - if (interfaceNames.length > 0) { + if (apexSortedByType["interface"].length > 0) { // Filter out interfaces packageClasses = packageClasses.filter( (packageClass) => { - for (let interfaceName of interfaceNames) { - if (interfaceName === packageClass) { + for (let interfaceClass of apexSortedByType["interface"]) { + if (interfaceClass["name"] === packageClass) { return false; } } - return true; }); }