diff --git a/README.md b/README.md index de9eaa93..98e2d3ae 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ Options: -w, --write [value] file path to write results -e, --excluded [value] The comma separated list of tests to exclude (default: none) -x, --externalPluginsDirectory [value] Relative or full path to an external plugins directory + -q, --quiet do not emit the report to stdout. (can use --write option to write to file) --list do not execute, instead list the available plugins and formatters --maxWarnings [value] Number of warnings to trigger nonzero exit code (default: -1) --profile [value] Either apigee or apigeex (default: apigee) @@ -66,21 +67,62 @@ Options: ``` Example: ``` -apigeelint -s sampleProxy/ -f table.js +apigeelint -s sampleProxy/apiproxy -f table.js ``` Where `-s` points to the apiProxy source directory and `-f` is the output formatter desired. Possible formatters are: "json.js" (the default), "stylish.js", "compact.js", "codeframe.js", "html.js", "table.js", "unix.js", "visualstudio.js", "checkstyle.js", "jslint-xml.js", "junit.js" and "tap.js". -Example Using External Plugins: +### More Examples + +#### Using External Plugins: +``` +apigeelint -x ./externalPlugins -s path/to/your/apiproxy -f table.js +``` +Where `-x` points to the directory containing externally developed plugins. + +You could, for example, create your own plugin for naming conventions, and +exclude the builtin plugin that enforces naming conventions (`PO007`) with the +`-e` option: + +``` +apigeelint -x ./externalPlugins -e PO007 -s path/to/your/apiproxy -f table.js ``` -apigeelint -x ./externalPlugins -e PO007 -s test/fixtures/resources/sampleProxy/24Solver/apiproxy -f table.js + +This would effectively override the built-in naming conventions that apigeelint checks. + + +#### Excluding plugins + +You can, of course, exclude plugins without providing a replacement implementation: + ``` -Where `-x` points to the directory containing externally developed plugins and `-e` excludes the builtin plugin from executing. -This example uses the "externalPlugins" directory with a plugin for alternate policy naming conventions and effectively overrides the built in naming conventions plugin. The output will include the external plugin identifier `EX-PO007`. +apigeelint -s path/to/your/apiproxy -f table.js -e PO007,ST003 +``` + +The above would exclude the policy naming convention check (`PO007`), and would +also not check for conditions on an ExtractVariables with a JSONPayload +(`ST003`), if for some reason you wanted to do that. + + +#### Writing output to a file +``` +apigeelint -s sampleProxy/apiproxy -f table.js -w existing-outputdir --quiet +``` + +The `-w` option can point to an existing directory, in which case the output +will be emitted to a file named apigeelint.out in that directory, in whatever +format you specify with `-f`. An existing file by that name will be overwritten. If the +`-w` option is not a directory, it is treated as the name of a file, and output +is written there. + +If you do not also specify `--quiet` the report will go to both stdout and to +the specified filesystem destination. + + -### Listing plugins +#### Listing plugins List plugins and formatters, with or without --externalPluginsDirectory. ```sh apigeelint --list diff --git a/cli.js b/cli.js index 8d66e8f4..e94a93d3 100755 --- a/cli.js +++ b/cli.js @@ -33,6 +33,7 @@ program // .option("-p, --password [value]", "Apigee password") // .option("-o, --organization [value]", "Apigee organization") .option("-x, --externalPluginsDirectory [value]", "Relative or full path to an external plugins directory") + .option("-q, --quiet", "do not emit the report to stdout. (can use --write option to write to file)") .option("--list", "do not execute, instead list the available plugins and formatters") .option("--maxWarnings [value]", "Number of warnings to trigger nonzero exit code (default: -1)") .option("--profile [value]", "Either apigee or apigeex (default: apigee)"); @@ -74,6 +75,10 @@ if (program.formatter) { configuration.formatter = program.formatter || "json.js"; } +if (program.quiet) { + configuration.output = 'none'; +} + if (program.excluded && typeof(program.excluded) === "string") { configuration.excluded = program .excluded @@ -91,4 +96,3 @@ if (program.profile) { } bl.lint(configuration); - diff --git a/lib/package/bundleLinter.js b/lib/package/bundleLinter.js index 89e32499..733deac5 100644 --- a/lib/package/bundleLinter.js +++ b/lib/package/bundleLinter.js @@ -16,12 +16,12 @@ /* global process */ const fs = require("fs"), - path = require("path"), - Bundle = require("./Bundle.js"), - pluralize = require("pluralize"), - myUtil = require("./myUtil.js"), - debug = require("debug")("apigeelint:bundleLinter"), - getcb = myUtil.curry(myUtil.diagcb, debug); + path = require("path"), + Bundle = require("./Bundle.js"), + pluralize = require("pluralize"), + myUtil = require("./myUtil.js"), + debug = require("debug")("apigeelint:bundleLinter"), + getcb = myUtil.curry(myUtil.diagcb, debug); function contains(a, obj, f) { if (!a || !a.length) { @@ -29,11 +29,11 @@ function contains(a, obj, f) { } f = f || - function(lh, rh) { + function (lh, rh) { return lh === rh; }; - for (var i = 0; i < a.length; i++) { + for (let i = 0; i < a.length; i++) { if (f(a[i], obj)) { if (!a[i]) { return true; @@ -44,39 +44,45 @@ function contains(a, obj, f) { return false; } -function exportData(path, report) { - fs.writeFile( - path + "apigeeLint.json", - JSON.stringify(report, null, 4), - "utf8", - function(err) { - if (err) { - return console.log(err); - } +function exportReport(providedPath, stringifiedReport) { + const constructedPath = + fs.existsSync(providedPath) && fs.lstatSync(providedPath).isDirectory() + ? path.join(providedPath, "apigeelint.out") + : providedPath; + fs.writeFile(constructedPath, stringifiedReport, "utf8", function (err) { + if (err) { + return console.log(err); } - ); + return null; + }); } const getPluginPath = () => path.resolve(path.join(__dirname, "plugins")); const listPlugins = () => fs.readdirSync(getPluginPath()).filter(resolvePlugin); -const listRuleIds = () => listPlugins().map(s => s.substring(0, 5)); +const listRuleIds = () => listPlugins().map((s) => s.substring(0, 5)); -const listExternalRuleIds = (externalDir) => fs.readdirSync(externalDir).filter(resolvePlugin).map(s => s.substring(0, 8)); +const listExternalRuleIds = (externalDir) => + fs + .readdirSync(externalDir) + .filter(resolvePlugin) + .map((s) => s.substring(0, 8)); const listFormatters = () => - fs.readdirSync(path.join(__dirname,'third_party/formatters')).filter( s => s.endsWith('.js')); + fs + .readdirSync(path.join(__dirname, "third_party/formatters")) + .filter((s) => s.endsWith(".js")); -var lint = function(config, done) { - return new Bundle(config, function(bundle, err) { +const lint = function (config, done) { + return new Bundle(config, function (bundle, err) { if (err) { - return (done) ? done(null, err) : console.log(err); + return done ? done(null, err) : console.log(err); } // for each builtin plugin - let normalizedPath = getPluginPath(); - fs.readdirSync(normalizedPath).forEach(function(file) { + const normalizedPath = getPluginPath(); + fs.readdirSync(normalizedPath).forEach(function (file) { if (!config.plugins || contains(config.plugins, file)) { try { executePlugin(normalizedPath + "/" + file, bundle); @@ -87,40 +93,35 @@ var lint = function(config, done) { }); // for each external plugin - if( config.externalPluginsDirectory ) { - fs.readdirSync(config.externalPluginsDirectory).forEach(function(file) { - if (!config.plugins || contains(config.plugins, file)) { - try { - executePlugin(path.resolve(config.externalPluginsDirectory) + "/" + file, bundle); - } catch (e) { - debug("plugin error: " + file + " " + e); - } - } - }); + if (config.externalPluginsDirectory) { + fs.readdirSync(config.externalPluginsDirectory).forEach(function (file) { + if (!config.plugins || contains(config.plugins, file)) { + try { + executePlugin( + path.resolve(config.externalPluginsDirectory) + "/" + file, + bundle + ); + } catch (e) { + debug("plugin error: " + file + " " + e); + } + } + }); } - var fmt = config.formatter || "json.js", - fmtImpl = getFormatter(fmt), - fmtReport = fmtImpl(bundle.getReport()); + const formatter = config.formatter || "json.js", + formatterImpl = getFormatter(formatter), + formattedReport = formatterImpl(bundle.getReport()); if (config.output) { if (typeof config.output == "function") { - config.output(fmtReport); + config.output(formattedReport); } } else { - console.log(fmtReport); - } - - if (fmt !== "json.js") { - (fmt = "json.js"), - (fmtImpl = getFormatter(fmt)), - (fmtReport = JSON.parse(fmtImpl(bundle.getReport()))); - } else { - fmtReport = JSON.parse(fmtReport); + console.log(formattedReport); } if (config.writePath) { - exportData(config.writePath, fmtReport); + exportReport(config.writePath, formattedReport); } if (done) { @@ -128,8 +129,8 @@ var lint = function(config, done) { } // Exit code should return 1 when there are errors - if (typeof config.setExitCode == 'undefined' || config.setExitCode) { - bundle.getReport().some(function(value){ + if (typeof config.setExitCode == "undefined" || config.setExitCode) { + bundle.getReport().some(function (value) { if (value.errorCount > 0) { process.exitCode = 1; return; @@ -137,22 +138,23 @@ var lint = function(config, done) { }); // Exit code should return 1 when more than maximum number of warnings allowed - if(config.maxWarnings >=0){ + if (config.maxWarnings >= 0) { let warningCount = 0; - bundle.getReport().forEach(report => warningCount += report.warningCount); - if(warningCount > config.maxWarnings){ + bundle + .getReport() + .forEach((report) => (warningCount += report.warningCount)); + if (warningCount > config.maxWarnings) { process.exitCode = 1; return; } } } - }); }; -var getFormatter = function(format) { +var getFormatter = function (format) { // default is stylish - var formatterPath; + let formatterPath; format = format || "stylish.js"; if (typeof format === "string") { @@ -177,61 +179,69 @@ var getFormatter = function(format) { }; const bfnName = (term) => - (term == 'Bundle') ? 'onBundle' : pluralize('on' + term, 2); + term == "Bundle" ? "onBundle" : pluralize("on" + term, 2); -const pluginIdRe1 = new RegExp('^[A-Z]{2}[0-9]{3}$'); -const pluginIdRe2 = new RegExp('^_.+\.js$'); +const pluginIdRe1 = new RegExp("^[A-Z]{2}[0-9]{3}$"); +const pluginIdRe2 = new RegExp("^_.+.js$"); /* exposed for testing */ -const resolvePlugin = idOrFilename => { - if (idOrFilename.endsWith(".js")) { - if (idOrFilename.indexOf('/') < 0) { - if ( ! pluginIdRe2.test(idOrFilename)) { - debug(`resolvePlugin file(${idOrFilename}) , prepending path...`); - return path.resolve(getPluginPath(), idOrFilename); - } - else return null; - } - else { - return idOrFilename; - } - } - else if (pluginIdRe1.test(idOrFilename)) { - let p = fs.readdirSync(path.resolve(getPluginPath())) - .filter( p => p.startsWith(idOrFilename) && p.endsWith(".js")); - if (p.length>1) { - throw new Error("plugin conflict: " + JSON.stringify(p)); - } - return p.length ? path.resolve(getPluginPath(), p[0]) : null; - } - return null; - }; - -const executePlugin = function(file, bundle) { - let pluginPath = resolvePlugin(file); - - if (pluginPath) { - debug(`executePlugin file(${pluginPath})`); - let plugin = require(pluginPath); - if (plugin.plugin.enabled && (!bundle.excluded || bundle.excluded[plugin.plugin.ruleId]!==true)) { - debug(`execPlugin ${pluginPath}`); - let basename = path.basename(pluginPath).slice(0, -3), - entityTypes = ['Bundle', 'Step', 'Condition', - 'ProxyEndpoint', 'TargetEndpoint', - 'Resource', 'Policy', 'FaultRule', - 'DefaultFaultRule']; - - entityTypes.forEach( etype => { - let pfn = plugin['on' + etype]; - if (pfn) { - let label =`plugin ${basename} on${etype}`; - debug(label + ' start'); - bundle[bfnName(etype)](pfn, getcb(label)); - } - }); +const resolvePlugin = (idOrFilename) => { + if (idOrFilename.endsWith(".js")) { + if (idOrFilename.indexOf("/") < 0) { + if (!pluginIdRe2.test(idOrFilename)) { + debug(`resolvePlugin file(${idOrFilename}) , prepending path...`); + return path.resolve(getPluginPath(), idOrFilename); + } else return null; + } else { + return idOrFilename; + } + } else if (pluginIdRe1.test(idOrFilename)) { + let p = fs + .readdirSync(path.resolve(getPluginPath())) + .filter((p) => p.startsWith(idOrFilename) && p.endsWith(".js")); + if (p.length > 1) { + throw new Error("plugin conflict: " + JSON.stringify(p)); + } + return p.length ? path.resolve(getPluginPath(), p[0]) : null; + } + return null; +}; + +const executePlugin = function (file, bundle) { + let pluginPath = resolvePlugin(file); + + if (pluginPath) { + debug(`executePlugin file(${pluginPath})`); + let plugin = require(pluginPath); + if ( + plugin.plugin.enabled && + (!bundle.excluded || bundle.excluded[plugin.plugin.ruleId] !== true) + ) { + debug(`execPlugin ${pluginPath}`); + let basename = path.basename(pluginPath).slice(0, -3), + entityTypes = [ + "Bundle", + "Step", + "Condition", + "ProxyEndpoint", + "TargetEndpoint", + "Resource", + "Policy", + "FaultRule", + "DefaultFaultRule" + ]; + + entityTypes.forEach((etype) => { + let pfn = plugin["on" + etype]; + if (pfn) { + let label = `plugin ${basename} on${etype}`; + debug(label + " start"); + bundle[bfnName(etype)](pfn, getcb(label)); } - } - }; + }); + } + } +}; module.exports = { lint,