diff --git a/lib/common/files.js b/lib/common/files.js
index 47ca039..c3dbd23 100644
--- a/lib/common/files.js
+++ b/lib/common/files.js
@@ -299,6 +299,9 @@ var FileManager = function(baseURI, relBaseURI, console, flow){
this.warns = [];
this.readInfo = [];
+
+ // helpers
+ this.abspath = abspath;
};
FileManager.prototype = {
diff --git a/lib/extract/js/l10n.js b/lib/extract/js/l10n.js
index 70455e7..5ad71bf 100644
--- a/lib/extract/js/l10n.js
+++ b/lib/extract/js/l10n.js
@@ -36,6 +36,34 @@ module.exports = function(file, flow, defineHandler, globalScope){
fconsole.log('[basis.l10n] basis.l10n.Dictionary#token.compute ' + id);
token_.obj = resolveL10nToken(key + '.{?}', dictPath).jsToken.obj;
+ }),
+ token: at.createRunner(function(token_, this_, args, scope){
+ var tokenKey = scope.simpleExpression(args[0]);
+
+ if (tokenKey && tokenKey.type == 'Literal')
+ {
+ tokenKey = tokenKey.value;
+
+ var file = tokenDescriptor.dictionary.file;
+ var id = key + '.' + tokenKey + '@' + file.filename;
+ // collect through refs
+ var parts = tokenKey.split('.');
+
+ fconsole.log('[basis.l10n] basis.l10n.Token#token ' + id);
+ parts.reduce(function(prev, current){
+ var l10nToken = flow.l10n.getToken(key + '.' + prev + '@' + file.filename);
+
+ l10nToken.addRef(this.file, token_, 'through');
+
+ return prev + '.' + current;
+ }.bind(this));
+
+ // add explicit ref
+ var l10nToken = resolveL10nToken(key + '.' + tokenKey, file.filename);
+
+ l10nToken.addRef(this.file, token_, 'explicit');
+ token_.obj = l10nToken.jsToken.obj;
+ }
})
};
@@ -63,12 +91,25 @@ module.exports = function(file, flow, defineHandler, globalScope){
if (key && key.type == 'Literal')
{
- var id = key.value + '@' + file.filename;
+ key = key.value;
+
+ // collect through refs
+ var parts = key.split('.');
+ var id = key + '@' + file.filename;
fconsole.log('[basis.l10n] basis.l10n.Dictionary#token ' + id);
+ parts.reduce(function(prev, current){
+ var l10nToken = flow.l10n.getToken(prev + '@' + file.filename);
+
+ l10nToken.addRef(this.file, token_, 'through');
+
+ return prev + '.' + current;
+ }.bind(this));
+
+ // add explicit ref
+ var l10nToken = resolveL10nToken(key, file.filename);
- var l10nToken = resolveL10nToken(key.value, file.filename);
- l10nToken.addRef(this.file, token_);
+ l10nToken.addRef(this.file, token_, 'explicit');
token_.obj = l10nToken.jsToken.obj;
}
else
@@ -161,7 +202,7 @@ module.exports = function(file, flow, defineHandler, globalScope){
var filename = scope.simpleExpression(args[0]);
if (filename && filename.type == 'Literal')
{
- //filename = this.file.resolve(filename[1]);
+ // filename = this.file.resolve(filename.value);
filename = basisResolveURI
? basisResolveURI(filename.value, flow.indexFile.baseURI)
: resolveToBase(flow, filename.value, flow.indexFile.baseURI);
diff --git a/lib/extract/l10n/Token.js b/lib/extract/l10n/Token.js
index 15d2dbb..945efde 100644
--- a/lib/extract/l10n/Token.js
+++ b/lib/extract/l10n/Token.js
@@ -10,14 +10,17 @@ var Token = function(dictionary, name){
Token.prototype.type = 'default';
Token.prototype.comment = null;
-Token.prototype.addRef = function(file, refToken){
+Token.prototype.addRef = function(file, refToken, type){
for (var i = 0, ref; ref = this.ref[i]; i++)
if (ref.file === file && ref.refToken === refToken)
return;
+ // explicit has more priority than through
+ this.usage = type === 'explicit' ? type : this.usage || type;
this.ref.push({
file: file,
- refToken: refToken
+ refToken: refToken,
+ type: type
});
};
diff --git a/lib/extract/l10n/index.js b/lib/extract/l10n/index.js
index 143c4b2..99a8660 100644
--- a/lib/extract/l10n/index.js
+++ b/lib/extract/l10n/index.js
@@ -25,6 +25,17 @@ var tmplAt = require('basisjs-tools-ast').tmpl;
if (l10nPrefix.test(bindName))
{
var l10nTokenRef = bindName.substr(5);
+ var parts = l10nTokenRef.split('@');
+ var tokenName = parts[0];
+ var dictFilename = parts[1];
+ var tokenNameParts = tokenName.match(/^(.+?)\.{(.+?)}/);
+
+ if (tokenNameParts && tokenNameParts.length == 3) {
+ tokenName = tokenNameParts[1];
+ }
+
+ l10nTokenRef = tokenName + '@' + dictFilename;
+
var l10nToken = flow.l10n.getToken(l10nTokenRef);
var name = l10nToken.name;
var dictionary = l10nToken.dictionary;
@@ -43,7 +54,17 @@ var tmplAt = require('basisjs-tools-ast').tmpl;
dictionary.file.jsRefCount++;
dictionary.addRef(this.file);
this.file.link(dictionary.file);
- l10nToken.addRef(this.file, tmplRef);
+
+ // collect through refs
+ tokenName.split('.').reduce(function(prev, current){
+ var l10nToken = flow.l10n.getToken(prev + '@' + dictFilename);
+
+ l10nToken.addRef(this.file, tmplRef, 'through');
+
+ return prev + '.' + current;
+ }.bind(this));
+ // add explicit ref
+ l10nToken.addRef(this.file, tmplRef, 'explicit');
tmplRefs.push(tmplRef);
}
@@ -55,6 +76,17 @@ var tmplAt = require('basisjs-tools-ast').tmpl;
if (l10nPrefix.test(bindName))
{
var l10nTokenRef = bindName.substr(5);
+ var parts = l10nTokenRef.split('@');
+ var tokenName = parts[0];
+ var dictFilename = parts[1];
+ var tokenNameParts = tokenName.match(/^(.+?)\.{(.+?)}/);
+
+ if (tokenNameParts && tokenNameParts.length == 3) {
+ tokenName = tokenNameParts[1];
+ }
+
+ l10nTokenRef = tokenName + '@' + dictFilename;
+
var l10nToken = flow.l10n.getToken(l10nTokenRef);
var name = l10nToken.name;
var dictionary = l10nToken.dictionary;
@@ -73,7 +105,17 @@ var tmplAt = require('basisjs-tools-ast').tmpl;
dictionary.file.jsRefCount++;
dictionary.addRef(this.file);
this.file.link(dictionary.file);
- l10nToken.addRef(this.file, tmplRef);
+
+ // collect through refs
+ tokenName.split('.').reduce(function(prev, current){
+ var l10nToken = flow.l10n.getToken(prev + '@' + dictFilename);
+
+ l10nToken.addRef(this.file, tmplRef, 'through');
+
+ return prev + '.' + current;
+ }.bind(this));
+ // add explicit ref
+ l10nToken.addRef(this.file, tmplRef, 'explicit');
tmplRefs.push(tmplRef);
}
diff --git a/lib/lint/command.js b/lib/lint/command.js
index 37ed6a1..a7493dc 100644
--- a/lib/lint/command.js
+++ b/lib/lint/command.js
@@ -2,6 +2,7 @@ var path = require('path');
var clap = require.main.require('clap');
var common = require('../common/command');
var isChildProcess = typeof process.send == 'function'; // child process has send method
+var handleUnusedL10n = require('./reporter/parallel-process-unused-l10n');
function resolveCwd(value){
return path.resolve(process.env.PWD || process.cwd(), value);
@@ -20,6 +21,7 @@ module.exports = clap.create('lint', '[fileOrPreset]')
.option('--no-color', 'Suppress color output')
.option('--silent', 'No any output')
+ .option('--warn-unused-l10n
', 'Warn about unused l10n tokens for specified path. Avoid using with --js-cut-dev since it might cause to incorrect results')
.option('--filter ', 'Show warnings only for specified file', resolveCwd)
.option('-r, --reporter ', 'Reporter console (default), checkstyle, junit',
function(reporter){
@@ -53,6 +55,8 @@ module.exports.getParallelOptions = function(){
return {
silent: true,
callback: function(res){
+ handleUnusedL10n(res);
+
var reporter = require(require('./reporter')[command.values.reporter]);
var data = require('./reporter/parallel-process-warns.js')(res);
console.log(reporter(data));
diff --git a/lib/lint/index.js b/lib/lint/index.js
index 8a73339..2634d5a 100644
--- a/lib/lint/index.js
+++ b/lib/lint/index.js
@@ -7,6 +7,7 @@ var extract = require('../extract');
var command = require('./command');
var chalk = require('chalk');
var isChildProcess = typeof process.send == 'function'; // child process has send method
+var unusedL10n = require('./unused/l10n');
if (isChildProcess)
process.on('uncaughtException', function(error){
@@ -91,6 +92,13 @@ function lint(config){
l10nInfo: true
}).concat([
function(flow){
+ if (options.warnUnusedL10n)
+ {
+ flow.usedL10nTokens = unusedL10n.collectUsed(flow);
+ if (!isChildProcess)
+ unusedL10n.warn(flow);
+ }
+
flow.result = require('./reporter/process-warns')(flow.warns, options.filter);
}
]);
@@ -148,6 +156,7 @@ function lint(config){
event: 'done',
success: !flow.warns.length,
warnings: flow.warns,
+ usedL10nTokens: flow.usedL10nTokens,
result: flow.result
});
});
diff --git a/lib/lint/reporter/parallel-process-unused-l10n.js b/lib/lint/reporter/parallel-process-unused-l10n.js
new file mode 100644
index 0000000..30d2403
--- /dev/null
+++ b/lib/lint/reporter/parallel-process-unused-l10n.js
@@ -0,0 +1,60 @@
+var path = require('path');
+
+module.exports = function handleUnusedL10n(tasks){
+ var usedL10nTokens = {};
+ var unusedL10nTokens = {};
+
+ // merge used l10n tokens from tasks
+ // and collect used and unused l10n tokens
+ tasks.forEach(function(task){
+ if (task.result.usedL10nTokens)
+ for (var dictFileName in task.result.usedL10nTokens.items)
+ {
+ var absDictFilename = task.result.usedL10nTokens.basePath + dictFileName;
+ var tokenUsageInfo = task.result.usedL10nTokens.items[dictFileName];
+
+ usedL10nTokens[absDictFilename] = usedL10nTokens[absDictFilename] || {};
+ unusedL10nTokens[absDictFilename] = unusedL10nTokens[absDictFilename] || {};
+
+ for (var tokenName in tokenUsageInfo)
+ if (tokenUsageInfo.hasOwnProperty(tokenName))
+ {
+ var used = tokenUsageInfo[tokenName];
+ var alreadyUsed = usedL10nTokens[absDictFilename].hasOwnProperty(tokenName);
+
+ if (alreadyUsed)
+ continue;
+
+ if (!used)
+ unusedL10nTokens[absDictFilename][tokenName] = true;
+ else
+ {
+ usedL10nTokens[absDictFilename][tokenName] = true;
+ delete unusedL10nTokens[absDictFilename][tokenName];
+ }
+ }
+
+ if (!Object.keys(unusedL10nTokens[absDictFilename]).length)
+ delete unusedL10nTokens[absDictFilename];
+ }
+ });
+
+ // warn about unused l10n tokens
+ if (Object.keys(unusedL10nTokens).length)
+ {
+ for (var dictFileName in unusedL10nTokens)
+ {
+ var tokenNames = unusedL10nTokens[dictFileName];
+
+ dictFileName = path.relative(process.cwd(), dictFileName);
+ tasks.push({
+ name: 'unused l10n tokens at ' + dictFileName,
+ result: {
+ warnings: Object.keys(tokenNames).map(function(name){
+ return { file: 'Path "' + name + '" defined but never used' };
+ })
+ }
+ });
+ }
+ }
+};
diff --git a/lib/lint/reporter/parallel-process-warns.js b/lib/lint/reporter/parallel-process-warns.js
index 70f3309..66bdc89 100644
--- a/lib/lint/reporter/parallel-process-warns.js
+++ b/lib/lint/reporter/parallel-process-warns.js
@@ -17,7 +17,7 @@ module.exports = function(tasks){
failures.push({
loc: warn.loc,
- message: warn.message + ' at ' + filename
+ message: warn.message ? warn.message + ' at ' + filename : filename
});
});
});
diff --git a/lib/lint/unused/l10n.js b/lib/lint/unused/l10n.js
new file mode 100644
index 0000000..5aefd22
--- /dev/null
+++ b/lib/lint/unused/l10n.js
@@ -0,0 +1,102 @@
+function isTarget(flow, basePath, collectPath, file){
+ return file.filename && (basePath + file.filename).indexOf(collectPath + '/') === 0 && !flow.ignoreWarning(file.filename);
+}
+
+var TOKEN_REF_UNUSED = 0;
+var TOKEN_REF_THROUGH = 1;
+var TOKEN_REF_IMPLICIT = 2;
+var TOKEN_REF_EXPLICIT = 3;
+
+var usageMap = {
+ through: TOKEN_REF_THROUGH,
+ implicit: TOKEN_REF_IMPLICIT,
+ explicit: TOKEN_REF_EXPLICIT
+};
+
+exports.collectUsed = function(flow){
+ var options = flow.options;
+ var basePath = options.base;
+ var collectPath = flow.files.abspath(basePath, options.warnUnusedL10n);
+ var usedTokens = {};
+
+ if (!flow.l10n || !flow.l10n.dictionaries)
+ return;
+
+ for (var dictFilename in flow.l10n.dictionaries)
+ {
+ if (isTarget(flow, basePath, collectPath, { filename: dictFilename }))
+ {
+ var dict = flow.l10n.dictionaries[dictFilename];
+ var dictTokens = dict.tokens;
+
+ // mark tokens as explicitly, implicitly or not used
+ for (var tokenName in dictTokens)
+ {
+ var tokenInfo = dictTokens[tokenName];
+
+ usedTokens[dictFilename] = usedTokens[dictFilename] || {};
+
+ if (tokenInfo.ref.length)
+ usedTokens[dictFilename][tokenName] = usageMap[tokenInfo.usage];
+ else
+ usedTokens[dictFilename][tokenName] = TOKEN_REF_UNUSED;
+ }
+
+ // mark unused tokens in explicitly used branches as implicitly used
+ for (tokenName in usedTokens[dictFilename])
+ {
+ if (!usedTokens[dictFilename][tokenName])
+ {
+ var parts = tokenName.split('.');
+ var passed = [];
+
+ if (parts.length > 1)
+ {
+ // collect branches till explicitly used branch
+ while (parts.length)
+ {
+ var currentPath = parts.join('.');
+
+ if (usedTokens[dictFilename][currentPath] === TOKEN_REF_EXPLICIT)
+ break;
+
+ passed.push(currentPath);
+ parts.pop();
+ }
+
+ // if we have an explicitly used branch then mark collected branches as implicitly used
+ if (parts.length)
+ for (var i = 0; i < passed.length; i++)
+ usedTokens[dictFilename][passed[i]] = TOKEN_REF_IMPLICIT;
+ }
+ }
+ }
+ }
+ }
+
+ return {
+ basePath: basePath,
+ collectPath: collectPath,
+ items: usedTokens
+ };
+};
+
+exports.warn = function(flow){
+ if (flow.options.warnUnusedL10n && flow.usedL10nTokens)
+ {
+ var usedL10nTokensInfo = flow.usedL10nTokens;
+
+ for (var dictFileName in usedL10nTokensInfo.items){
+ var tokenNames = usedL10nTokensInfo.items[dictFileName];
+
+ for (var tokenName in tokenNames) {
+ if (!tokenNames[tokenName]) {
+ flow.warn({
+ file: dictFileName,
+ message: 'Path "' + tokenName + '" defined but never used'
+ });
+ }
+ }
+ }
+ }
+};