Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make line number selection work with @ava/babel & @ava/typescript #2473

Closed
novemberborn opened this issue Apr 26, 2020 · 5 comments
Closed

Comments

@novemberborn
Copy link
Member

Line number selection relies on AVA parsing the test file. This does not work when you use a require hook (such as ts-node/register). That's OK. However we do want it to work with our Babel and TypeScript providers.

Ultimately, we need start and end lines & columns for each call expression in the test file. First we need to initialize the providers earlier:

// TODO: Initialize providers here, then pass to lineNumberSelection() so they
// can be used to parse the test file.
let checkSelectedByLineNumbers;
try {
checkSelectedByLineNumbers = lineNumberSelection({
file: options.file,
lineNumbers: options.lineNumbers
});

See here:

const extensionsToLoadAsModules = Object.entries(options.moduleTypes)
.filter(([, type]) => type === 'module')
.map(([extension]) => extension);
// Install before processing options.require, so if helpers are added to the
// require configuration the *compiled* helper will be loaded.
const {projectDir, providerStates = []} = options;
const providers = providerStates.map(({type, state}) => {
if (type === 'babel') {
const provider = providerManager.babel(projectDir).worker({extensionsToLoadAsModules, state});
runner.powerAssert = provider.powerAssert;
return provider;
}
if (type === 'typescript') {
return providerManager.typescript(projectDir).worker({extensionsToLoadAsModules, state});
}
return null;
}).filter(provider => provider !== null);

Then, depending on the extension of the test file, we need to get the provider to give us the call locations. For normal JS files you can find that logic here:

function parse(file) {
const fs = require('fs');
const acorn = require('acorn');
const walk = require('acorn-walk');
const ast = acorn.parse(fs.readFileSync(file, 'utf8'), {
ecmaVersion: 11,
locations: true
});
const locations = [];
walk.simple(ast, {
CallExpression(node) {
locations.push(node.loc);
}
});
// Walking is depth-first, but we want to sort these breadth-first.
locations.sort((a, b) => {
if (a.start.line === b.start.line) {
return a.start.column - b.start.column;
}
return a.start.line - b.start.line;
});
return locations;
}

For @ava/babel we need to parse the test file using Babel, with all the configured plugins active and whatnot.

For @ava/typescript, hopefully we can use typescript itself as a parser?

@oantoro
Copy link
Contributor

oantoro commented Jun 26, 2020

Current implementation of @ava/babel manages source map through its worker() method. It installs source-map-support at line https://github.com/avajs/babel/blob/0949eaadfff2c7f8009f056c62c5f632ff7f0c6d/index.js#L359
I think it is good to move source map management functionality to AVA. So the providers can focus to:

  1. compile and cache source file
  2. load precompiled module
  3. parse source file and also add additional function canParse(file) to check if the provider can parse the given file

By moving source map management to AVA, we will get more flexibility to manage the stack trace. Maybe we could create new file source-map-manager.js which wraps source-map-support functionality. The implementation of source-map-support internally modifies the Error.prepareStackTrace https://github.com/evanw/node-source-map-support/blob/d29e9c81527346b6ec385f7ba27a7a1c487b69c7/source-map-support.js#L563 to generate original stack trace. In the future we could replace source-map-support with explicit implementation to solve #2474
The source-map-manager.js is not coupled with test worker, so we also can use it in main process as long as the state of compiled source which hold source map location is provided

ava/lib/api.js

Lines 194 to 197 in 78cfaa1

const providerStates = (await Promise.all(providers.map(async ({type, main}) => {
const state = await main.compile({cacheDir, files: testFiles});
return state === null ? null : {type, state};
}))).filter(state => state !== null);

Example of source map manager usage:

const SourceMapManager = require('./source-map-manager');

const sourceMapManager = await new SourceMapManager({
  defaultStackTraceInference: 'compiled-stack'
});

sourceMapManager.installSourceMapHandler();

sourceMapManager.addState(state); // register `state` which is generated by provider, where `source-map-manager` could find the source map file

 // this can be used to replace `const sourceCallSite = sourceMapSupport.wrapCallSite(callSite);` in `worker/line-numbers.js`
// it will decide which source map to use and return original frame
sourceMapManager.getOriginalFrame(frame);

sourceMapManager.enableOriginalStackTrace(); // enable source map manager to infer original stack
const originalStack = (new Error('foo')).stack
sourceMapManager.disableOriginalStackTrace(); // leave source map manager to infer from compiled source again

// maybe also expose some lower level methods such as methods found in `SourceMapConsumer`

We can pass sourceMapManager to lineNumberSelection() along with providers so we can use to make line number selection work.

const getParser = providers => {
  for (const provider of providers) {
    if (provider.canParse(file)) {
      return provider.parse;
    }
  }
  // no provider can handle. use default
  return parse;
}

parse = getParser(providers);
const locations = parse(file);
// omitted lines
// ......
return () => {
  // omitted lines
  // .....
  const sourceCallSite = sourceMapManager.getOriginalFrame(callSite);
  const start = {
    line: sourceCallSite.getLineNumber(),
    column: sourceCallSite.getColumnNumber() - 1
  };

  const test = findTest(locations, start);
  // omitted lines
  // .....
}

In the future maybe we could add more providers such as coffee-script etc. Correct me if my interpretation is wrong.

@novemberborn
Copy link
Member Author

I think it is good to move source map management functionality to AVA.

@okyantoro yes, see arno #2474.

That said I'd prefer using a language specific AST to determine the line numbers.

In the future maybe we could add more providers such as coffee-script etc. Correct me if my interpretation is wrong.

I don't think it's wrong. AVA itself can focus on Node.js support, and other languages / dialects can be handled through providers. However we have limited resources and should refrain from supporting esoteric platforms.

@alastairs
Copy link

I hit this issue/#2474 yesterday with some tests I wrote in Typescript, and worked around it by using Babel to transpile my Typescript into Javascript for AVA to execute. It's... clunky. I see both issues are labelled help wanted - what help is needed at this point?

@novemberborn
Copy link
Member Author

@alastairs I think the issue description is still accurate. Happy to help if you're getting stuck anywhere in particular.

This was referenced May 28, 2021
This was referenced Jun 6, 2021
@novemberborn novemberborn unpinned this issue Oct 31, 2021
@novemberborn
Copy link
Member Author

AVA 4 removes @ava/babel. I think #2859 may have fixed this issue for TypeScript.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants