Skip to content

Commit

Permalink
v0.0.7
Browse files Browse the repository at this point in the history
Move to Tree-Sitter Web (WASM) to increase compat
  • Loading branch information
Anthony Heber committed Sep 6, 2023
1 parent 37dc3ff commit 935379b
Show file tree
Hide file tree
Showing 7 changed files with 2,011 additions and 1,195 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@
},
"editor.tabSize": 2,
"editor.formatOnSave": true,
"rewrap.wrappingColumn": 80
"rewrap.wrappingColumn": 80,
"typescript.tsdk": "./node_modules/typescript/lib"
}
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
{
"name": "sf-warp",
"description": "Warp your code and see if your tests notice",
"version": "0.0.6",
"version": "0.0.7",
"dependencies": {
"@oclif/core": "^1.16.3",
"@salesforce/core": "^3.25.0",
"@salesforce/kit": "^1.5.45",
"@salesforce/sf-plugins-core": "^1.14.1",
"tree-sitter": "^0.20.0",
"tree-sitter-sfapex": "^0.0.7",
"tslib": "^2"
"tslib": "^2",
"web-tree-sitter-sfapex": "^0.0.9"
},
"devDependencies": {
"@oclif/test": "^2.1.1",
Expand Down
74 changes: 45 additions & 29 deletions src/lib/commands/apex.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable complexity */
/* eslint-disable no-console */
import * as fs from 'fs';
import * as path from 'path';
import * as process from 'node:process';
import { Connection } from '@salesforce/core';
import * as Parser from 'tree-sitter';
import * as tsApex from 'tree-sitter-sfapex';
import { getApexParser } from 'web-tree-sitter-sfapex';
import { Query, QueryCapture, Tree } from 'web-tree-sitter';
import { getApexClasses, executeTests, writeApexClassesToOrg, ApexClassRecord } from '../sf';
import { getTextParts, Lines, isTestClass, TSCapture } from '../ts_tools';
import { getTextParts, Lines, isTestClass } from '../ts_tools';
import { getMutatedParts } from '../mutations';
import { getPerfStart, getPerfDurationMs, getPerfDurationHumanReadable } from '../perf';

const queries = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'tsQueries', 'apexCaptures.scm'));
const queries = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'tsQueries', 'apexCaptures.scm'), 'utf-8');

type Parser = Awaited<ReturnType<typeof getApexParser>>;

export enum Verbosity {
none = 'none',
Expand Down Expand Up @@ -43,6 +41,17 @@ interface Config {
}>;
}

interface Mutant {
type: string;
startLine: number;
startPosition: number;
testResults;
status: string;
error: string;
deploymentDurationMs: number;
testExecuteDurationMs: number;
}

const isVerboseEnough = (val: Verbosity, minimumVerbosity: Verbosity): boolean => {
return VerbosityVal[val] >= VerbosityVal[minimumVerbosity];
};
Expand All @@ -60,11 +69,11 @@ export default class ApexWarper {

private classMapByName: { [key: string]: ApexClassRecord } = {};

private mutants = {};
private mutants = new Map<string, Mutant[]>();

private conn: Connection;

private parser;
private parser: Parser;

private orgClassIsMutated = false;
private unwindingPromise: Promise<void>;
Expand All @@ -74,9 +83,8 @@ export default class ApexWarper {
this.config = { ...this.config, ...config };
}

public async executeWarpTests(): Promise<any> {
this.parser = new Parser();
this.parser.setLanguage((await tsApex).apex);
public async executeWarpTests(): Promise<{ [k: string]: Mutant[] }> {
this.parser = await getApexParser();
for (const classUnderTest of this.config.classes) {
const totalExecutePerfName = getPerfStart();
if (!classUnderTest.testClasses) {
Expand Down Expand Up @@ -115,10 +123,12 @@ export default class ApexWarper {
console.log('Parsed in', getPerfDurationHumanReadable(parsePerfName));
}

const query = new Parser.Query(this.parser.getLanguage(), queries);
const query = this.parser.getLanguage().query(queries);

// TODO: Need to figure out how to block mutants inside of "ignore" sections
const captures = this.getCaptures(tree, query).filter((c) => !this.config.suppressedRuleNames.includes(c.name));
const captures = this.getCaptures(tree, query).filter(
(c) => !(this.config.suppressedRuleNames || []).includes(c.name)
);

if (this.atLeastVerbosity(Verbosity.minimal)) {
console.log(
Expand All @@ -127,19 +137,19 @@ export default class ApexWarper {
}
let mutantsKilled = 0;
let count = 0;
this.mutants[className] = [];
this.mutants.set(className, []);
for (const capture of captures) {
let finalStatus = 'unknown';
let finalStatusMessage;
let finalStatusMessage: string;
const perfName = getPerfStart();

const oldLines: Lines = {};
for (let i = capture.node.startPosition.row; i <= capture.node.endPosition.row; i++) {
oldLines[i] = lines[i];
lines[i] = ''; // blank out the line so it is easier to inject replacements later
}
let deployDuration;
let testPerfDuration;
let deployDuration: number;
let testPerfDuration: number;
try {
const textParts = getMutatedParts(capture, oldLines);
if (this.atLeastVerbosity(Verbosity.details)) {
Expand Down Expand Up @@ -189,14 +199,19 @@ export default class ApexWarper {
finalStatus = 'survived';
}
} catch (error) {
let errorMessage = 'Unknown Error';
// TODO: if in minimal mode, buffer these and drop after all tests are done
finalStatus = 'failure';
if (this.atLeastVerbosity(Verbosity.minimal)) {
console.log('Failure:', error.mesage || error);

if (error instanceof Error) {
errorMessage = error.message;
if (this.atLeastVerbosity(Verbosity.minimal)) {
console.log('Failure:', error.message || error);
}
}
finalStatusMessage = error.message;
finalStatusMessage = errorMessage;
}
this.mutants[className].push({
this.mutants.get(className).push({
type: capture.name,
startLine: capture.node.startPosition.row,
startPosition: capture.node.startPosition.column,
Expand Down Expand Up @@ -230,21 +245,21 @@ export default class ApexWarper {
await this.writeApexClassesToOrg(classUnderTest.className, originalClassText);
this.orgClassIsMutated = false;
}
return this.mutants;
return Object.fromEntries(this.mutants);
}

private getCaptures(tree, query): TSCapture[] {
private getCaptures(tree: Tree, query: Query): QueryCapture[] {
const queryPerfName = getPerfStart();

const captures = query.captures(tree.rootNode) as TSCapture[];
const captures = query.captures(tree.rootNode);

if (this.atLeastVerbosity(Verbosity.full)) {
console.log('Query executed in', getPerfDurationHumanReadable(queryPerfName));
}
return captures;
}

private reportMutant(capture: TSCapture, oldText: Lines, newLineParts: string[]): void {
private reportMutant(capture: QueryCapture, oldText: Lines, newLineParts: string[]): void {
// probably a smarter way to do this out there...
const [start, middle, end] = getTextParts(oldText, capture.node);
console.log(
Expand Down Expand Up @@ -284,13 +299,14 @@ export default class ApexWarper {
return isVerboseEnough(this.config.verbosity, val);
}

private async writeApexClassesToOrg(className: string, body: string): Promise<any> {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
private async writeApexClassesToOrg(className: string, body: string) {
// If the system received a stop request and started unwinding, don't deploy again
// will process exit as part of that promise so this is really just forcing a stop
if (this.unwindingPromise !== undefined) {
await this.unwindingPromise;
}
return writeApexClassesToOrg(this.conn, this.classMapByName[className].Id, body, this.config.timeoutMs);
return writeApexClassesToOrg(this.conn, this.classMapByName[className].Id || '', body, this.config.timeoutMs);
}

private async usePatternsToGuessAtTestClasses(classUnderTest: {
Expand Down
9 changes: 4 additions & 5 deletions src/lib/mutations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { TSCapture } from './ts_tools';
import { Lines } from './ts_tools';
import { getTextParts } from './ts_tools';
import { QueryCapture } from 'web-tree-sitter';
import { getTextParts, Lines } from './ts_tools';

export enum MUTANT_TYPES {
ADDITION_SUBTRACT = 'addition_subtract',
Expand All @@ -22,15 +21,15 @@ export enum MUTANT_TYPES {
RETURN_WITH_VALUE = 'return_with_value',
}

export function getMutatedParts(capture: TSCapture, text: Lines): string[] {
export function getMutatedParts(capture: QueryCapture, text: Lines): string[] {
// if start and end are on different lines, capture from start position to end of line,
// then if needed, full lines, until finally 0 until end of capture
const [start, capturedText, end] = getTextParts(text, capture.node);
return [start, getMutantValue(capture, capturedText), end];
}

// eslint-disable-next-line complexity
export function getMutantValue(capture: TSCapture, text: string): string {
export function getMutantValue(capture: QueryCapture, text: string): string {
switch (capture.name) {
case MUTANT_TYPES.ADDITION_SUBTRACT:
return text === '+' ? '-' : '+';
Expand Down
50 changes: 5 additions & 45 deletions src/lib/ts_tools.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,15 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import * as Parser from 'tree-sitter';
import * as tsApex from 'tree-sitter-sfapex';

interface TSLocation {
row: number;
column: number;
}

interface TSNode {
startPosition: TSLocation;
endPosition: TSLocation;
}

export interface TSCapture {
name: string;
node: TSNode;
}
interface TSQuery {
captures(TSTreeNode): TSCapture[];
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface TSLanguage {}
// interface TSTree {
// soemthing();
// }
interface TSParser {
setLanguage(TSLanguage): void;
getLanguage(): TSLanguage;
parse(
input: string | Parser.Input | Parser.InputReader,
oldTree?: Parser.Tree,
options?: {
bufferSize?: number;
includedRanges?: Parser.Range[];
}
): Parser.Tree;
}
import { getApexParser } from 'web-tree-sitter-sfapex';
import { SyntaxNode } from 'web-tree-sitter';
export interface Lines {
[key: number]: string;
}

export async function isTestClass(classBody: string): Promise<boolean> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
const parser = new Parser() as TSParser;
parser.setLanguage((await tsApex).apex);
const parser = await getApexParser();

const tree = parser.parse(classBody);

const query = new Parser.Query(parser.getLanguage(), '(annotation name: (identifier) @annotationNames)') as TSQuery;
const query = parser.getLanguage().query('(annotation name: (identifier) @annotationNames)');

const testCandidateCaptures = query.captures(tree.rootNode);
const testLines = classBody.split('\n');
Expand All @@ -68,7 +28,7 @@ export async function isTestClass(classBody: string): Promise<boolean> {
return isTest;
}

export function getTextParts(lines: Lines, node: TSNode): string[] {
export function getTextParts(lines: Lines, node: SyntaxNode): string[] {
// probably a smarter way to do this out there...
const line = lines[node.startPosition.row];
if (node.startPosition.row === node.endPosition.row) {
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"extends": "@salesforce/dev-config/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
"rootDir": "src",
"skipLibCheck": true
},
"include": ["./src/**/*.ts"]
}
Loading

0 comments on commit 935379b

Please sign in to comment.