diff --git a/lib/ATGetDependencies.js b/lib/ATGetDependencies.js index 21c56bb..117b2fb 100755 --- a/lib/ATGetDependencies.js +++ b/lib/ATGetDependencies.js @@ -15,6 +15,7 @@ var UglifyJS = require("uglify-js"); var grunt = require('./grunt').grunt(); +var findGlobals = require('./findGlobals'); var acceptedAriaMethods = { 'classDefinition' : 1, @@ -43,25 +44,45 @@ var getLogicalPath = { TXT : createGetLogicalPathFn('.tpl.txt') }; -var appendString = function (stringLitteral, mapFunction, array) { +var removeResolvedGlobals = function (globals, classpath) { + var useful = false; + for (var i = globals.length - 1; i >= 0; i--) { + var curGlobal = globals[i]; + if ((curGlobal.length == classpath.length || curGlobal.charAt(classpath.length) == ".") && + curGlobal.substring(0, classpath.length) == classpath) { + useful = true; + globals.splice(i, 1); + } + } + return useful; +}; + +var appendString = function (stringLitteral, mapFunction, state, alwaysUseful) { if (stringLitteral instanceof UglifyJS.AST_String) { - var value = mapFunction(stringLitteral.value); - array.push(value); + var classpath = stringLitteral.value; + var value = mapFunction(classpath); + state.declaredDependencies.push(value); + if (state.unresolvedGlobals) { + var useful = removeResolvedGlobals(state.unresolvedGlobals, classpath) || alwaysUseful; + if (!useful) { + state.uselessDependencies.push(value); + } + } } else { reportError('Expected a string litteral.', stringLitteral); } }; -var appendMappedStringLitterals = function (stringLitterals, mapFunction, array) { +var appendMappedStringLitterals = function (stringLitterals, mapFunction, state, alwaysUseful) { for (var i = 0, l = stringLitterals.length; i < l; i++) { - appendString(stringLitterals[i], mapFunction, array); + appendString(stringLitterals[i], mapFunction, state, alwaysUseful); } }; -var appendMappedMapValueStringLitterals = function (object, mapFunction, array) { +var appendMappedMapValueStringLitterals = function (object, mapFunction, state, alwaysUseful) { var properties = object.properties; for (var i = 0, l = properties.length; i < l; i++) { - appendString(properties[i].value, mapFunction, array); + appendString(properties[i].value, mapFunction, state, alwaysUseful); } }; @@ -99,10 +120,10 @@ var checkAriaDefinition = function (walker) { return false; }; -var handleArray = function (mapFunction) { - return function (value, res, walker) { +var handleArray = function (mapFunction, alwaysUseful) { + return function (value, state, walker) { if (value instanceof UglifyJS.AST_Array) { - appendMappedStringLitterals(value.elements, mapFunction, res); + appendMappedStringLitterals(value.elements, mapFunction, state, alwaysUseful); } else { reportError('Expected an array litteral', walker.self()); } @@ -110,9 +131,9 @@ var handleArray = function (mapFunction) { }; var handleMap = function (mapFunction) { - return function (value, res, walker) { + return function (value, state, walker) { if (value instanceof UglifyJS.AST_Object) { - appendMappedMapValueStringLitterals(value, mapFunction, res); + appendMappedMapValueStringLitterals(value, mapFunction, state, true); } else { reportError('Expected an object litteral', walker.self()); } @@ -120,8 +141,13 @@ var handleMap = function (mapFunction) { }; var processNames = { + '$classpath' : function (value, state) { + if (value instanceof UglifyJS.AST_String && state.unresolvedGlobals) { + removeResolvedGlobals(state.unresolvedGlobals, value.value); + } + }, '$dependencies' : handleArray(getLogicalPath.JS), - '$extends' : function (value, res, walker) { + '$extends' : function (value, state, walker) { var extendsType; var extendsTypeValue = findMapValue(walker.parent(0), '$extendsType'); if (extendsTypeValue && extendsTypeValue instanceof UglifyJS.AST_String) { @@ -130,12 +156,12 @@ var processNames = { if (!getLogicalPath.hasOwnProperty(extendsType)) { extendsType = "JS"; } - appendString(value, getLogicalPath[extendsType], res); + appendString(value, getLogicalPath[extendsType], state, true); }, - '$implements' : handleArray(getLogicalPath.JS), + '$implements' : handleArray(getLogicalPath.JS, true), '$namespaces' : handleMap(getLogicalPath.JS), - '$css' : handleArray(getLogicalPath.CSS), - '$templates' : handleArray(getLogicalPath.TPL), + '$css' : handleArray(getLogicalPath.CSS, true), + '$templates' : handleArray(getLogicalPath.TPL, true), '$texts' : handleMap(getLogicalPath.TXT) }; @@ -144,17 +170,31 @@ var processNames = { * @param {Object} ast uglify-js abstract syntax tree * @return {Array} array of dependencies */ -var getATDependencies = function (ast) { - var res = []; +var getATDependencies = function (ast, options) { + options = options || {}; + var state = { + declaredDependencies : [] + }; + if (options.checkGlobals !== false) { + state.unresolvedGlobals = findGlobals(ast, { + ignoreBuiltin : options.ignoreBuiltinGlobals, + includesWith : options.includesWithGlobals + }); + state.uselessDependencies = []; + var resolvedGlobals = (options.resolvedGlobals || []).concat("Aria"); + for (var i = 0, l = resolvedGlobals.length; i < l; i++) { + removeResolvedGlobals(state.unresolvedGlobals, resolvedGlobals[i]); + } + } var walker = new UglifyJS.TreeWalker(function (node) { if (node instanceof UglifyJS.AST_ObjectProperty && processNames.hasOwnProperty(node.key)) { if (checkAriaDefinition(walker)) { - processNames[node.key](node.value, res, walker); + processNames[node.key](node.value, state, walker); } } }); ast.walk(walker); - return res; + return state; }; module.exports = getATDependencies; \ No newline at end of file diff --git a/lib/findGlobals.js b/lib/findGlobals.js new file mode 100644 index 0000000..9518232 --- /dev/null +++ b/lib/findGlobals.js @@ -0,0 +1,92 @@ +/* + * Copyright 2013 Amadeus s.a.s. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var UglifyJS = require("uglify-js"); + +var inContext = function () { + var global = this; + var undef; + return function (name) { + return global[name] !== undef; + }; +}; + +var isVarBuiltin = function (name) { + var vm = require("vm"); + // run in an empty context to know which are the built-in globals + isVarBuiltin = vm.runInNewContext("(" + inContext + ")()"); + return isVarBuiltin(name); +}; + +var isUndeclaredSymbolRef = function (node, walker, options) { + if (!(node instanceof UglifyJS.AST_SymbolRef && node.thedef.undeclared)) { + return false; + } + if (node.name == "arguments" && walker.find_parent(UglifyJS.AST_Lambda)) { + return false; + } + if (options.ignoreBuiltin && isVarBuiltin(node.name)) { + return false; + } + return true; +}; + +var matchStart = function (testName) { + return function (acceptedPrefix) { + return (testName.length == acceptedPrefix.length || testName.charAt(acceptedPrefix.length) == ".") && + testName.substring(0, acceptedPrefix.length) == acceptedPrefix; + }; +}; + +var checkWith = function (name, walker, i, options) { + if (options.includesWith === true) { + return true; + } + var stack = walker.stack; + for (var j = i; j >= 0; j--) { + var curItem = stack[j]; + if (curItem instanceof UglifyJS.AST_With && curItem.body === stack[j + 1]) { + if (Array.isArray(options.includesWith)) { + return options.includesWith.some(matchStart(name)); + } + return false; + } + } + // not in a with (...) {...} structure + return true; +}; + +module.exports = function (ast, options) { + options = options || {}; + var res = {}; + ast.figure_out_scope(); + var walker = new UglifyJS.TreeWalker(function (node) { + if (isUndeclaredSymbolRef(node, walker, options)) { + var stack = walker.stack; + var i = stack.length - 2; + while (i >= 0 && stack[i] instanceof UglifyJS.AST_Dot) { + i--; + } + i++; + var wholeProperty = stack[i]; + var name = wholeProperty.print_to_string(); + if (checkWith(name, walker, i, options)) { + res[name] = true; + } + } + }); + ast.walk(walker); + return Object.keys(res).sort(); +}; \ No newline at end of file diff --git a/lib/visitors/ATDependencies.js b/lib/visitors/ATDependencies.js index 915af35..e057963 100755 --- a/lib/visitors/ATDependencies.js +++ b/lib/visitors/ATDependencies.js @@ -24,6 +24,18 @@ var ATDependencies = function (cfg) { this.files = cfg.files || ['**/*']; this.mustExist = cfg.hasOwnProperty('mustExist') ? cfg.mustExist : true; this.externalDependencies = cfg.hasOwnProperty('externalDependencies') ? cfg.externalDependencies : []; + this.unresolvedGlobalError = cfg.hasOwnProperty('unresolvedGlobalError') ? cfg.unresolvedGlobalError : false; + this.unresolvedGlobalWarning = cfg.hasOwnProperty('unresolvedGlobalWarning') ? cfg.unresolvedGlobalWarning : true; + this.uselessDependencyError = cfg.hasOwnProperty('uselessDependencyError') ? cfg.uselessDependencyError : false; + this.uselessDependencyWarning = cfg.hasOwnProperty('uselessDependencyWarning') ? cfg.uselessDependencyWarning + : false; + this.atDependenciesOptions = { + ignoreBuiltinGlobals : cfg.hasOwnProperty('ignoreBuiltinGlobals') ? cfg.ignoreBuiltinGlobals : true, + includesWithGlobals : cfg.hasOwnProperty('includesWithGlobals') ? cfg.includesWithGlobals : false, + resolvedGlobals : cfg.hasOwnProperty('resolvedGlobals') ? cfg.resolvedGlobals : [], + checkGlobals : this.unresolvedGlobalError || this.unresolvedGlobalWarning || this.uselessDependencyError || + this.uselessDependencyWarning + }; }; ATDependencies.prototype.computeDependencies = function (packaging, inputFile) { @@ -46,8 +58,20 @@ ATDependencies.prototype.computeDependencies = function (packaging, inputFile) { var externalDependencies = this.externalDependencies; var ast = uglifyContentProvider.getAST(inputFile, jsStringContent); if (ast) { - var dependencies = atGetDependencies(ast); - dependencies.forEach(function (dependency) { + var logMethod; + var depInfo = atGetDependencies(ast, this.atDependenciesOptions); + if ((this.unresolvedGlobalWarning || this.unresolvedGlobalError) && depInfo.unresolvedGlobals.length > 0) { + logMethod = this.unresolvedGlobalError ? "error" : "warn"; + grunt.log[logMethod](inputFile.logicalPath.yellow + " uses the following unresolved globals:\n - " + + depInfo.unresolvedGlobals.join("\n - ")); + } + if ((this.uselessDependencyError || this.uselessDependencyWarning) && depInfo.uselessDependencies.length > 0) { + logMethod = this.unresolvedGlobalError ? "error" : "warn"; + grunt.log[logMethod](inputFile.logicalPath.yellow + + " depends on the following files without using them (apparently):\n - " + + depInfo.uselessDependencies.join("\n - ")); + } + depInfo.declaredDependencies.forEach(function (dependency) { var correspondingFile = packaging.getSourceFile(dependency); if (correspondingFile) { inputFile.addDependency(correspondingFile);