Skip to content

Commit

Permalink
feat(instrumenter): allow override of babel plugins
Browse files Browse the repository at this point in the history
Allow an override of babel plugins for js files. This uses the 'old' `mutator.pl
  • Loading branch information
nicojs committed Jul 10, 2020
1 parent 8ceff31 commit 8758cfd
Show file tree
Hide file tree
Showing 27 changed files with 562 additions and 132 deletions.
12 changes: 8 additions & 4 deletions packages/core/src/process/2-MutantInstrumenterExecutor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injector, tokens, commonTokens } from '@stryker-mutator/api/plugin';
import { Instrumenter, InstrumentResult } from '@stryker-mutator/instrumenter';
import { File } from '@stryker-mutator/api/core';
import { File, MutatorDescriptor } from '@stryker-mutator/api/core';

import { MainContext, coreTokens } from '../di';
import InputFileCollection from '../input/InputFileCollection';
Expand All @@ -20,15 +20,19 @@ export interface MutantInstrumenterContext extends MainContext {
}

export class MutantInstrumenterExecutor {
public static readonly inject = tokens(commonTokens.injector, coreTokens.inputFiles);
constructor(private readonly injector: Injector<MutantInstrumenterContext>, private readonly inputFiles: InputFileCollection) {}
public static readonly inject = tokens(commonTokens.injector, coreTokens.inputFiles, commonTokens.mutatorDescriptor);
constructor(
private readonly injector: Injector<MutantInstrumenterContext>,
private readonly inputFiles: InputFileCollection,
private readonly mutatorDescriptor: MutatorDescriptor
) {}

public async execute(): Promise<Injector<DryRunContext>> {
// Create the checker and instrumenter
const instrumenter = this.injector.injectClass(Instrumenter);

// Instrument files in-memory
const instrumentResult = await instrumenter.instrument(this.inputFiles.filesToMutate);
const instrumentResult = await instrumenter.instrument(this.inputFiles.filesToMutate, { plugins: this.mutatorDescriptor.plugins });

// Rewrite tsconfig file references
const tsconfigFileRewriter = this.injector.injectClass(SandboxTSConfigRewriter);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import sinon = require('sinon');
import { expect } from 'chai';
import { File } from '@stryker-mutator/api/core';
import { File, MutatorDescriptor } from '@stryker-mutator/api/core';
import { Injector } from 'typed-inject';
import { factory } from '@stryker-mutator/test-helpers';
import { Instrumenter, InstrumentResult } from '@stryker-mutator/instrumenter';
import { Instrumenter, InstrumentResult, InstrumenterOptions } from '@stryker-mutator/instrumenter';
import { Checker } from '@stryker-mutator/api/check';

import { MutantInstrumenterExecutor } from '../../../src/process';
Expand All @@ -26,6 +26,7 @@ describe(MutantInstrumenterExecutor.name, () => {
let mutatedFile: File;
let originalFile: File;
let testFile: File;
let mutatorDescriptor: MutatorDescriptor;

beforeEach(() => {
mutatedFile = new File('foo.js', 'console.log(global.activeMutant === 1? "": "bar")');
Expand All @@ -44,7 +45,8 @@ describe(MutantInstrumenterExecutor.name, () => {
sandboxTSConfigRewriterMock.rewrite.resolves([mutatedFile, testFile]);
inputFiles = new InputFileCollection([originalFile, testFile], [mutatedFile.name]);
injectorMock = factory.injector();
sut = new MutantInstrumenterExecutor(injectorMock, inputFiles);
mutatorDescriptor = factory.mutatorDescriptor({ plugins: ['functionSent'] });
sut = new MutantInstrumenterExecutor(injectorMock, inputFiles, mutatorDescriptor);
injectorMock.injectClass.withArgs(Instrumenter).returns(instrumenterMock);
injectorMock.injectClass.withArgs(SandboxTSConfigRewriter).returns(sandboxTSConfigRewriterMock);
injectorMock.injectFunction.withArgs(Sandbox.create).returns(sandboxMock);
Expand All @@ -58,7 +60,8 @@ describe(MutantInstrumenterExecutor.name, () => {

it('should instrument the given files', async () => {
await sut.execute();
expect(instrumenterMock.instrument).calledOnceWithExactly([originalFile]);
const expectedInstrumenterOptions: InstrumenterOptions = { plugins: ['functionSent'] };
expect(instrumenterMock.instrument).calledOnceWithExactly([originalFile], expectedInstrumenterOptions);
});

it('result in the new injector', async () => {
Expand Down
1 change: 1 addition & 0 deletions packages/instrumenter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@types/babel__generator": "^7.6.1",
"@types/babel__parser": "^7.1.1",
"@types/chai-jest-snapshot": "^1.3.5",
"babel-plugin-transform-decorators-legacy": "^1.3.5",
"chai-jest-snapshot": "^2.0.0"
}
}
1 change: 1 addition & 0 deletions packages/instrumenter/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './instrumenter';
export * from './instrument-result';
export * from './instrumenter-options';
3 changes: 3 additions & 0 deletions packages/instrumenter/src/instrumenter-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ParserOptions } from './parsers';

export interface InstrumenterOptions extends ParserOptions {}
6 changes: 4 additions & 2 deletions packages/instrumenter/src/instrumenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { tokens, commonTokens } from '@stryker-mutator/api/plugin';
import { Logger } from '@stryker-mutator/api/logging';
import { File } from '@stryker-mutator/api/core';

import { parse } from './parsers';
import { createParser } from './parsers';
import { transform, MutantCollector } from './transformers';
import { print } from './printers';
import { InstrumentResult } from './instrument-result';
import { InstrumenterOptions } from './instrumenter-options';

/**
* The instrumenter is responsible for
Expand All @@ -21,11 +22,12 @@ export class Instrumenter {

constructor(private readonly logger: Logger) {}

public async instrument(files: readonly File[]): Promise<InstrumentResult> {
public async instrument(files: readonly File[], options: InstrumenterOptions): Promise<InstrumentResult> {
this.logger.debug('Instrumenting %d source files with mutants', files.length);
const mutantCollector = new MutantCollector();
const outFiles: File[] = [];
let mutantCount = 0;
const parse = createParser(options);
for await (const file of files) {
const ast = await parse(file.textContent, file.name);
transform(ast, mutantCollector);
Expand Down
28 changes: 17 additions & 11 deletions packages/instrumenter/src/parsers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,26 @@ import path from 'path';

import { AstFormat, AstByFormat } from '../syntax';

import { parse as jsParse } from './js-parser';
import { createParser as createJSParser } from './js-parser';
import { parse as tsParse } from './ts-parser';
import { parse as htmlParse } from './html-parser';
import { ParserOptions } from './parser-options';

export function parse<T extends AstFormat = AstFormat>(code: string, fileName: string, formatOverride?: T): Promise<AstByFormat[T]> {
const format = getFormat(fileName, formatOverride);
switch (format) {
case AstFormat.JS:
return jsParse(code, fileName) as Promise<AstByFormat[T]>;
case AstFormat.TS:
return tsParse(code, fileName) as Promise<AstByFormat[T]>;
case AstFormat.Html:
return htmlParse(code, fileName, { parse }) as Promise<AstByFormat[T]>;
}
export { ParserOptions };

export function createParser(parserOptions: ParserOptions) {
const jsParse = createJSParser(parserOptions);
return function parse<T extends AstFormat = AstFormat>(code: string, fileName: string, formatOverride?: T): Promise<AstByFormat[T]> {
const format = getFormat(fileName, formatOverride);
switch (format) {
case AstFormat.JS:
return jsParse(code, fileName) as Promise<AstByFormat[T]>;
case AstFormat.TS:
return tsParse(code, fileName) as Promise<AstByFormat[T]>;
case AstFormat.Html:
return htmlParse(code, fileName, { parse }) as Promise<AstByFormat[T]>;
}
};
}

function getFormat(fileName: string, override: AstFormat | undefined): AstFormat {
Expand Down
38 changes: 21 additions & 17 deletions packages/instrumenter/src/parsers/js-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { parseAsync, types } from '@babel/core';

import { AstFormat, JSAst } from '../syntax';

const plugins = [
import { ParserOptions } from './parser-options';

const defaultPlugins = [
'doExpressions',
'objectRestSpread',
'classProperties',
Expand All @@ -29,21 +31,23 @@ const plugins = [
['decorators', { decoratorsBeforeExport: false }],
] as ParserPlugin[];

export async function parse(text: string, fileName: string): Promise<JSAst> {
const ast = await parseAsync(text, {
parserOpts: {
plugins,
},
filename: fileName,
sourceType: 'module',
});
if (types.isProgram(ast)) {
throw new Error(`Expected ${fileName} to contain a babel.types.file, but was a program`);
}
return {
originFileName: fileName,
rawContent: text,
format: AstFormat.JS,
root: ast!,
export function createParser({ plugins: pluginsOverride }: ParserOptions) {
return async function parse(text: string, fileName: string): Promise<JSAst> {
const ast = await parseAsync(text, {
parserOpts: {
plugins: [...((pluginsOverride as ParserPlugin[]) ?? defaultPlugins)],
},
filename: fileName,
sourceType: 'module',
});
if (types.isProgram(ast)) {
throw new Error(`Expected ${fileName} to contain a babel.types.file, but was a program`);
}
return {
originFileName: fileName,
rawContent: text,
format: AstFormat.JS,
root: ast!,
};
};
}
3 changes: 3 additions & 0 deletions packages/instrumenter/src/parsers/parser-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface ParserOptions {
plugins: Array<string | unknown> | null;
}
2 changes: 2 additions & 0 deletions packages/instrumenter/src/parsers/ts-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export async function parse(text: string, fileName: string): Promise<TSAst> {
parserOpts: {
ranges: true,
},
configFile: false,
babelrc: false,
presets: [[require.resolve('@babel/preset-typescript'), { isTSX, allExtensions: true }]],
plugins: [[require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }]],
});
Expand Down
1 change: 1 addition & 0 deletions packages/instrumenter/src/printers/html-printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const print: Printer<HtmlAst> = (ast, context) => {
for (const script of sortedScripts) {
html += ast.rawContent.substring(currentIndex, script.root.start!);
html += '\n';
html += '// @ts-nocheck\n';
html += context.print(script, context);
html += '\n';
currentIndex = script.root.end!;
Expand Down
16 changes: 16 additions & 0 deletions packages/instrumenter/test/helpers/factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,25 @@ import { types } from '@babel/core';

import { JSAst, AstFormat, HtmlAst, TSAst } from '../../src/syntax';
import { Mutant, NamedNodeMutation } from '../../src/mutant';
import { ParserOptions } from '../../src/parsers';
import { InstrumenterOptions } from '../../src';

import { parseTS, parseJS, findNodePath } from './syntax-test-helpers';

export function createParserOptions(overrides?: Partial<ParserOptions>): ParserOptions {
return {
plugins: null,
...overrides,
};
}

export function createInstrumenterOptions(overrides?: Partial<InstrumenterOptions>): InstrumenterOptions {
return {
...createParserOptions(),
...overrides,
};
}

export function createHtmlAst(overrides?: Partial<HtmlAst>): HtmlAst {
return {
format: AstFormat.Html,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { expect } from 'chai';
import chaiJestSnapshot from 'chai-jest-snapshot';

import { Instrumenter } from '../../src';
import { createInstrumenterOptions } from '../helpers/factories';

const resolveTestResource = path.resolve.bind(
path,
Expand Down Expand Up @@ -43,10 +44,10 @@ describe('instrumenter integration', () => {
await arrangeAndActAssert('vue-sample.vue');
});

async function arrangeAndActAssert(fileName: string) {
async function arrangeAndActAssert(fileName: string, options = createInstrumenterOptions()) {
const fullFileName = resolveTestResource(fileName);
const file = new File(fullFileName, await fs.readFile(fullFileName));
const result = await sut.instrument([file]);
const result = await sut.instrument([file], options);
expect(result.files).lengthOf(1);
chaiJestSnapshot.setFilename(resolveTestResource(`${fileName}.out.snap`));
expect(result.files[0].textContent).matchSnapshot();
Expand Down
Loading

0 comments on commit 8758cfd

Please sign in to comment.