Skip to content

Commit

Permalink
Static analyzer: migration and lint tool (fix for #49 and #353)
Browse files Browse the repository at this point in the history
  • Loading branch information
miripiruni committed Jan 18, 2017
1 parent edac12f commit 0b3b876
Show file tree
Hide file tree
Showing 107 changed files with 1,646 additions and 2 deletions.
85 changes: 85 additions & 0 deletions migration/README.md
@@ -0,0 +1,85 @@
# Static Lint

Linting works the same way as migrate except you need add `lint` option.

`cd migration/ && npm i`

`cd ../`

`./migration/lib/index.js --lint --input ./path-to-templates/ --from 7 --to 8`

where
* `lint` lint option (if not present script will rewrite your templates)
* `input` path to templates (relative to current directory or absolute)
* `7` is major version from. If you specify `0` common check will be included.
* `8` is major to lint.

Result of linting is console warning like this:

```
BEM-XJST WARNING:
>>>> Function that returns a literal can be replaced with literal itself.
>>>> migration/tmpls/template.js:8
>>>> function() { return 42; }
```

# Migration tool

`cd migration/ && npm i`

`cd ../`

`./migration/lib/index.js --input ./path-to-templates/ --from 7 --to 8`

where
* `7` is current bem-xjst version on your project
* `8` is major version to migrate.

Notice that templates in `./path-to-templates/` will be overwritten.

## Codestyle config

You can create json file with several
[options](https://github.com/benjamn/recast/blob/52a7ec3eaaa37e78436841ed8afc948033a86252/lib/options.js#L1) that have recast.

Using `config` option you can pass path to json config:

`./migration/lib/index.js --input ./path-to-templates-dir --from 4 --to 8 --config ~/my-prj/config/codestyle-config.json`

Notice: path to json config must be absolute.

See example of codestyle config `sample-config.json` in this directory.

# List of transformers (checks)

* Array to function generator
* Object to function generator
* Don’t check `this.elemMods`
* Don’t check `this.mods`
* If function generator return simple type rewrite this function to it’s return value
* Check for HTML entities. Warning about using UTF-8 symbols.
* def() must return something
* No empty mode call
* No empty mode
* No more `this.`.
* Apply call to apply
* API changed (require('bem-xjst').bemhtml)
* `elemMatch()` to `elem()` with `match()`
* `mods` value
* `once()` to `def()`
* `this.isArray()` to `Array.isArray()`
* `xhtml: true` option for backwards capability (from 6 to 7)
* `attrs()` to `addAttrs()`
* `js()` to `addJs()`
* `mix()` to `addMix()`
* etc


# Tests

`npm test`


# Migrate from old DSL BEMHTML to JS syntax

Take a look at https://github.com/bem/bem-templates-converter
107 changes: 107 additions & 0 deletions migration/lib/index.js
@@ -0,0 +1,107 @@
#!/usr/bin/env node
'use strict';

var fs = require('fs');
var execSync = require('child_process').execSync;
var trDir = './migration/lib/transformers/';
var ls = fs.readdirSync(trDir);

var argv = require('yargs')
.option('input', {
describe: 'templates directory',
alias: 'i',
demand: true,
type: 'string'
})
.option('from', {
describe: 'current major version of bem-xjst',
alias: 'f',
default: '0',
type: 'string'
})
.option('to', {
describe: 'major version of bem-xjst to update for',
alias: 't',
default: '8',
type: 'string'
})
.option('lint', {
describe: 'lint mode',
default: false,
alias: 'l'
})
.option('config', {
describe: 'path to codestyle config for jscodeshift output',
alias: 'c'
})
.help('h')
.alias('help', 'h')
.argv;

var log = function log(arr) {
if (!Array.isArray(arr)) arr = [ arr ];
arr.push('\n');
console.log(arr.join('\n'));
};

if (argv.lint)
log('bem-xjst static linter started…');

var transformers = ls.filter(function(fileName) {
var major = fileName[0];

return major > argv.from && major <= argv.to;
})

var files = [ '*.bemhtml.js', '*.bemtree.js' ];

var formatExtensions = function formatExtensions(arr) {
return arr
.map(function(mask) { return '-iname "' + mask + '"'; })
.join(' -o ');
}

var input = argv.input;

if (input[input.length - 1] === '/')
input = input.substr(0, input.length - 1);

var cmd = 'find ' + input + ' ' + formatExtensions(files);

require('child_process').exec(cmd, function(err, stdout, stderr) {
var cmd;

if (stdout.length === 0) {
log('No ' + files.join(' or ') + ' files found.');
process.exit(1);
}

for (var i = 0; i < transformers.length; i++) {
cmd = [
'./migration/node_modules/jscodeshift/bin/jscodeshift.sh',
'-t ' + trDir + transformers[i],
stdout.split('\n').join(' '),
'--print'
];

if (argv.lint)
cmd.push('--lint=true');

if (argv.config) {
try {
var config = require(argv.config);
} catch(e) {
console.error('Error: cannot require config file from ' + argv.config);
console.error(e);
process.exit(1);
}

cmd.push('--config=' + argv.config);
}

cmd = cmd.join(' ');

log('Check rule: ' + transformers[i].replace(/^\d-/, '').replace(/-/g, ' ').replace(/\.js$/g, ''));
execSync(cmd, { stdio: 'inherit', encoding: 'utf8' })
}
});
14 changes: 14 additions & 0 deletions migration/lib/logger.js
@@ -0,0 +1,14 @@
module.exports = function logger(opts) {
'use strict';
var file = opts.file;
var path = opts.path;
var start = path.loc.start;

console.warn([
'BEM-XJST WARNING:',
opts.descr,
[ file.path, start.line, start.column ].join(':'),
file.source.slice(path.start - 5, path.end + 15),
'\n'
].join('\n>>>> '));
};
48 changes: 48 additions & 0 deletions migration/lib/transformer.js
@@ -0,0 +1,48 @@
var log = require('./logger');

function Transformer() {
this.init();
};

Transformer.prototype.init = function() {};

Transformer.prototype.description = '';

Transformer.prototype.replace = function(ret) {
return ret;
};

Transformer.prototype.find = function(file) {
return file;
};

Transformer.prototype.run = function(file, api, opts) {
var j = api.jscodeshift;
var ret = this.find(file, j);
var config = opts.config ? require(opts.config) : {};

if (!config.quote) config.quote = 'single';

if (opts.lint) {
if (ret.length === 0) return;
this.log(ret, file);
return;
}

return this.replace(ret, j).toSource(config);
};

Transformer.prototype.log = function(ret, file) {
var t = this;

ret.forEach(function(p) {
log({
descr: t.description,
path: p.value,
ret: ret,
file: file
});
});
};

module.exports = Transformer;
46 changes: 46 additions & 0 deletions migration/lib/transformers/0-arr-to-func-generator.js
@@ -0,0 +1,46 @@
var log = require('../logger');
var get = require('lodash.get');
var Transformer = require('../transformer');
var t = new Transformer();

module.exports = function(file, api, opts) {
t.description = 'Use function generator instead (Example: `function() { return []; }`. ' +
'See docs: https://github.com/bem/bem-xjst/wiki/Notable-changes-' +
'between-bem-xjst@1.x-and-bem-xjst@2.x#static-objects-shortcuts-in-mix-content-etc'

t.find = function(file, j) {
return j(file.source)
.find(j.ArrayExpression)
.filter(function(p) {
var arg = p.value.arguments;
var callee = get(p, 'parentPath.parentPath.value.callee.callee');

if (! callee)
return false;

callee = callee.name || (callee.property && callee.property.name);

return [
'js',
'mix',
'content',
'def',
'addMix',
'appendContent',
'prependContent'
].indexOf(callee) !== -1;
});
};

t.replace = function(ret, j) {
return ret.replaceWith(function(p) {
return j.functionExpression(
j.identifier(''),
[],
j.blockStatement([j.returnStatement(p.value)])
);
});
};

return t.run(file, api, opts);
};
26 changes: 26 additions & 0 deletions migration/lib/transformers/0-dont-check-this-elemmods.js
@@ -0,0 +1,26 @@
var log = require('../logger');
var Transformer = require('../transformer');
var t = new Transformer();

module.exports = function(file, api, opts) {
t.description = 'this.elemMods always exists. You don’t need to check it.';

t.find = function(file, j) {
return j(file.source)
.find(j.LogicalExpression, {
left: {
type: 'MemberExpression',
object: { type: 'ThisExpression' },
property: { name: 'elemMods' }
}
});
};

t.replace = function(ret, j) {
return ret.map(function(item) {
return item.replace(item.value.right);
});
};

return t.run(file, api, opts);
};
26 changes: 26 additions & 0 deletions migration/lib/transformers/0-dont-check-this-mods.js
@@ -0,0 +1,26 @@
var log = require('../logger');
var Transformer = require('../transformer');
var t = new Transformer();

module.exports = function(file, api, opts) {
t.description = 'this.mods always exists. You don’t need to check it.';

t.find = function(file, j) {
return j(file.source)
.find(j.LogicalExpression, {
left: {
type: 'MemberExpression',
object: { type: 'ThisExpression' },
property: { name: 'mods' }
}
});
};

t.replace = function(ret, j) {
return ret.map(function(item) {
return item.replace(item.value.right);
})
};

return t.run(file, api, opts);
};
26 changes: 26 additions & 0 deletions migration/lib/transformers/0-func-to-simple.js
@@ -0,0 +1,26 @@
var log = require('../logger');
var Transformer = require('../transformer');
var t = new Transformer();

module.exports = function(file, api, opts) {
t.description = 'Function that returns a literal can be replaced with literal itself.';

t.find = function(file, j) {
return j(file.source)
.find(j.FunctionExpression)
.filter(function(p) {
var body = p.node.body.body;
return body.length === 1 &&
body[0].type === 'ReturnStatement' &&
body[0].argument.type === 'Literal';
});
};

t.replace = function(ret, j) {
return ret.replaceWith(function(p) {
return j.literal(p.node.body.body[0].argument.value);
});
};

return t.run(file, api, opts);
};

0 comments on commit 0b3b876

Please sign in to comment.