Skip to content

Commit

Permalink
Merge pull request #12 from bttmly/better_filters
Browse files Browse the repository at this point in the history
Better filters
  • Loading branch information
bttmly authored May 16, 2019
2 parents 0f9473b + b4728d6 commit d3244c3
Show file tree
Hide file tree
Showing 13 changed files with 277 additions and 87 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ dogfood-fork: compile
node ./lib/cli -s lib -u fork

ci: compile
node ./lib/cli -s lib -k 85
node ./lib/cli -s lib -k 70

.PHONY: test example
62 changes: 62 additions & 0 deletions src/filters/filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as R from "ramda";
import * as escodegen from "escodegen";
import S from "../mutators/_syntax";
import { LocationFilter } from "../types";

export const isStringRequire = R.allPass([
R.propEq("type", S.CallExpression),
R.pathEq(["callee", "name"], "require"),
R.pathEq(["arguments", "length"], 1),
R.pathEq(["arguments", "0", "type"], S.Literal),
n => typeof R.path(["arguments", "0", "value"], n) === "string",
]);

export const isUseStrict = R.allPass([
R.propEq("type", S.ExpressionStatement),
R.pathEq(["expression", "value"], "use strict"),
]);

export const isCallOfName = (name: string): LocationFilter => {
return ({ node }) => {
if (!R.propEq("type", S.CallExpression, node)) return false;
if (!R.pathEq(["callee", "type"], S.Identifier, node)) return false;
if (!R.pathEq(["callee", "name"], name, node)) return false;
return true;
};
};

export function nodeSourceIncludes(text: string): LocationFilter {
return ({ node }) => {
return escodegen.generate(node).includes(text);
};
}

export function sourceNodeIncludesAny(texts: string[]): LocationFilter {
return ({ node }) => {
const code = escodegen.generate(node);
return texts.some(t => code.includes(t));
};
}

export function nodeSourceMatches(re: RegExp): LocationFilter {
return ({ node }) => {
return re.test(escodegen.generate(node));
};
}

export function sourceNodeMatchesAny(res: RegExp[]): LocationFilter {
return ({ node }) => {
const code = escodegen.generate(node);
return res.some(re => re.test(code));
};
}

export function sourceNodeIs(text: string): LocationFilter {
return ({ node }) => {
return escodegen.generate(node).trim() === text;
};
}

export const isESModuleInterop = nodeSourceIncludes(
"Object.defineProperty(exports, '__esModule', { value: true });",
);
19 changes: 1 addition & 18 deletions src/filters/index.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,9 @@
import * as R from "ramda";
import S from "../mutators/_syntax";
import { MutantLocation } from "../types";

type LocationFilter = (m: MutantLocation) => boolean;

const isStringRequire = R.allPass([
R.propEq("type", S.CallExpression),
R.pathEq(["callee", "name"], "require"),
R.pathEq(["arguments", "length"], 1),
R.pathEq(["arguments", "0", "type"], S.Literal),
n => typeof R.path(["arguments", "0", "value"], n) === "string",
]);

const isUseStrict = R.allPass([
R.propEq("type", S.ExpressionStatement),
R.pathEq(["expression", "value"], "use strict"),
]);

// const isCallOfName = (name: string) => R.allPass([
// R.pathEq(["expression", "callee", "type"], "Identifier"),
// R.pathEq(["expression", "callee", "name"], name),
// ]);
import { isUseStrict, isStringRequire } from "./filter";

const filters: LocationFilter[] = [
(m: MutantLocation) => !isUseStrict(m.node),
Expand Down
2 changes: 1 addition & 1 deletion src/matchers/contains-comparative.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const plugin: ComparativeMatcherPlugin = {
// TODO: lose the "!"s
const sourceName = withoutExt(sourceFile.split(perturbSourceDir).pop()!);
const testName = withoutExt(testFile.split(perturbTestDir).pop()!);
return testName.slice(0, sourceName.length) === sourceName;
return testName.startsWith(sourceName);
};
},
};
Expand Down
50 changes: 48 additions & 2 deletions src/mutators/_filters.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,49 @@
export function hasProp(prop: string) {
return (node: any) => node.hasOwnProperty(prop) && node[prop] !== null;
import * as R from "ramda";
import { NodeFilter } from "../types";
import * as escodegen from "escodegen";
import S from "../mutators/_syntax";

export function hasProp(prop: string): NodeFilter {
return node => {
return R.prop(prop, node as any) != null;
};
}

export const isCallOfName = (name: string): NodeFilter => {
return node => {
if (!R.propEq("type", S.CallExpression, node)) return false;
if (!R.pathEq(["callee", "type"], S.Identifier, node)) return false;
if (!R.pathEq(["callee", "name"], name, node)) return false;
return true;
};
};

export function nodeSourceIncludes(text: string): NodeFilter {
return node => escodegen.generate(node).includes(text);
}

export function sourceNodeIncludesAny(texts: string[]): NodeFilter {
return node => {
const code = escodegen.generate(node);
return texts.some(t => code.includes(t));
};
}

export function nodeSourceMatches(re: RegExp): NodeFilter {
return node => re.test(escodegen.generate(node));
}

export function sourceNodeMatchesAny(res: RegExp[]): NodeFilter {
return node => {
const code = escodegen.generate(node);
return res.some(re => re.test(code));
};
}

export function sourceNodeIs(text: string): NodeFilter {
return node => escodegen.generate(node).trim() === text;
}

export const isESModuleInterop = nodeSourceIncludes(
"Object.defineProperty(exports, '__esModule', { value: true });",
);
6 changes: 6 additions & 0 deletions src/mutators/_syntax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,10 @@ S.LOOP_NODES = [S.WhileStatement, S.DoWhileStatement, S.ForStatement];

S.TEST_NODES = [S.IfStatement, S.ConditionalExpression, S.SwitchCase];

S.FUNC_NODES = [
S.FunctionDeclaration,
S.FunctionExpression,
S.ArrowFunctionExpression,
];

export default S;
24 changes: 24 additions & 0 deletions src/mutators/drop-parameter-default.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as R from "ramda";
import S from "./_syntax";
import { VOID_NODE } from "./_constant-nodes";
import { MutatorPlugin } from "../types";

// a default parameter
// input: `function fn (x = 1) {}`
// output: `function fn (x) {}`

const plugin: MutatorPlugin = {
type: "mutator",
name: "drop-return",
nodeTypes: S.FUNC_NODES,
mutator: R.ifElse(
node => node.argument == null,
R.always(VOID_NODE),
node => ({
type: S.ExpressionStatement,
expression: node.argument,
}),
),
};

export default plugin;
20 changes: 17 additions & 3 deletions src/mutators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,11 @@ function makeMutatorIndex(names: string[]): MutatorIndex {
return index;
}

function locateMutatorPlugins(names: string[]): MutatorPlugin[] {
return names.map((name: string): MutatorPlugin => {
function locateMutatorPlugins(
names: string[],
strict = false,
): MutatorPlugin[] {
const plugins = names.map((name: string) => {
try {
const plugin: MutatorPlugin = require(`perturb-plugin-mutator-${name}`);
return plugin;
Expand All @@ -95,9 +98,20 @@ function locateMutatorPlugins(names: string[]): MutatorPlugin[] {
console.log(
`unable to locate -MUTATOR- plugin "${name}" -- fatal error, exiting`,
);
throw err;
if (strict) throw err;
}
return;
});

return removeNils<MutatorPlugin>(plugins);
}

function removeNils<T>(arr: Array<T | null | void>): T[] {
const xs: T[] = [];
for (const item of arr) {
if (item != null) xs.push(item);
}
return xs;
}

// exports.injectPlugins = function (names: string[]) {
Expand Down
8 changes: 1 addition & 7 deletions src/mutators/reverse-function-parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,12 @@ import S from "./_syntax";
import * as util from "./util";
import { MutatorPlugin } from "../types";

const FUNC_NODES = [
S.FunctionDeclaration,
S.FunctionExpression,
S.ArrowFunctionExpression,
];

// reverse the perameter order for a function expression or declaration
// `function fn (a, b) {}` => `function fn (b, a) {}`
const plugin: MutatorPlugin = {
type: "mutator",
name: "reverse-function-parameters",
nodeTypes: FUNC_NODES,
nodeTypes: S.FUNC_NODES,
filter: util.lengthAtPropGreaterThan("params", 1),
mutator: util.update("params", (ps: any[]) => ps.slice().reverse()),
};
Expand Down
36 changes: 28 additions & 8 deletions src/mutators/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,35 @@ export const update = R.curry(
// return updater(R.prop, obj).map(updated => R.assoc(prop, updated, obj));
// });

export const lengthAtPropGreaterThan = R.curry(
(prop: string, count: number, obj: object): boolean => {
return (R.path([prop, "length"], obj) as number) > count;
},
);
export const lengthAtPropGreaterThan = (prop: string, count: number) => {
return (obj: any) => (R.path([prop, "length"], obj) as number) > count;
};

// given an object with an array property, return an array of
// copies of that object, each copy having one of the array's
// elements removed

// dropEachOfProp("key", {key: [1, 2, 3]})
// => [ {key: [2, 3]}, {key: [1, 3]}, {key: [1, 2]} ]

export const dropEachOfProp = R.curry((prop: string, obj: any): any[] => {
const target: any[] = R.prop(prop, obj);
return target.map((_: any, i: number) => {
return R.assoc(prop, R.remove(i, 1, target), obj);
const arr: any[] = R.prop(prop, obj);
// TODO: runtime verify Array.isArray(arr)
return arr.map((_: any, i: number) => {
return R.assoc(prop, R.remove(i, 1, arr), obj);
});
});

export const dropEachOfPropPred = (prop: string, pred: (x: any) => boolean) => {
return (obj: any) => {
const results: any[] = [];
const arr: any[] = R.prop(prop, obj);
// TODO: runtime verify Array.isArray(target)
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
if (pred(item)) {
results.push(R.assoc(prop, R.remove(i, 1, arr), obj));
}
}
};
};
Loading

0 comments on commit d3244c3

Please sign in to comment.