From bc21afd424728591ea45eb8c1153df0a1b719a70 Mon Sep 17 00:00:00 2001 From: Glavin Wiechert Date: Sun, 28 May 2017 18:21:10 -0300 Subject: [PATCH] Add Executable class to abstract CLI beautifiers --- docs/index.coffee | 2 + package.json | 2 + spec/atom-beautify-spec.coffee | 7 +- spec/beautifier-php-cs-fixer-spec.coffee | 28 +- src/beautifiers/beautifier.coffee | 276 ++++---------------- src/beautifiers/executable.coffee | 309 +++++++++++++++++++++++ src/beautifiers/gherkin.coffee | 4 +- src/beautifiers/index.coffee | 56 ++-- src/beautifiers/php-cs-fixer.coffee | 38 ++- src/beautifiers/phpcbf.coffee | 11 + 10 files changed, 453 insertions(+), 280 deletions(-) create mode 100644 src/beautifiers/executable.coffee diff --git a/docs/index.coffee b/docs/index.coffee index 90c3e5c..61b4dbe 100755 --- a/docs/index.coffee +++ b/docs/index.coffee @@ -157,6 +157,8 @@ Handlebars.registerHelper('beautifiers-info', (beautifiers, options) -> rows = _.map(beautifiers, (beautifier, k) -> name = beautifier.name isPreInstalled = beautifier.isPreInstalled + if typeof isPreInstalled is "function" + isPreInstalled = beautifier.isPreInstalled() link = beautifier.link installationInstructions = if isPreInstalled then "Nothing!" else "Go to #{link} and follow the instructions." return "| #{name} | #{if isPreInstalled then ':white_check_mark:' else ':x:'} | #{installationInstructions} |" diff --git a/package.json b/package.json index 1f704c7..5234181 100644 --- a/package.json +++ b/package.json @@ -179,6 +179,8 @@ "pug-beautify": "^0.1.1", "remark": "^6.0.1", "season": "^5.3.0", + "semver": "^5.3.0", + "shell-env": "^0.3.0", "space-pen": "^5.1.1", "strip-json-comments": "^2.0.1", "temp": "^0.8.3", diff --git a/spec/atom-beautify-spec.coffee b/spec/atom-beautify-spec.coffee index fcd3db9..d128a7e 100644 --- a/spec/atom-beautify-spec.coffee +++ b/spec/atom-beautify-spec.coffee @@ -1,4 +1,5 @@ Beautifiers = require "../src/beautifiers" +Executable = require "../src/beautifiers/executable" beautifiers = new Beautifiers() Beautifier = require "../src/beautifiers/beautifier" Languages = require('../src/languages/') @@ -124,7 +125,7 @@ describe "Atom-Beautify", -> pathOption: "Lang - Test Program Path" } # Force to be Windows - beautifier.isWindows = true + Executable.isWindows = () ->true terminal = 'CMD prompt' whichCmd = "where.exe" # Process @@ -132,7 +133,7 @@ describe "Atom-Beautify", -> expect(p).not.toBe(null) expect(p instanceof beautifier.Promise).toBe(true) cb = (v) -> - # console.log(v) + console.log("error", v, v.description) expect(v).not.toBe(null) expect(v instanceof Error).toBe(true) expect(v.code).toBe("CommandNotFound") @@ -167,7 +168,7 @@ describe "Atom-Beautify", -> pathOption: "Lang - Test Program Path" } # Force to be Mac/Linux (not Windows) - beautifier.isWindows = false + Executable.isWindows = () ->false terminal = "Terminal" whichCmd = "which" # Process diff --git a/spec/beautifier-php-cs-fixer-spec.coffee b/spec/beautifier-php-cs-fixer-spec.coffee index 1e49e70..391bb15 100644 --- a/spec/beautifier-php-cs-fixer-spec.coffee +++ b/spec/beautifier-php-cs-fixer-spec.coffee @@ -1,5 +1,6 @@ PHPCSFixer = require "../src/beautifiers/php-cs-fixer" Beautifier = require "../src/beautifiers/beautifier" +Executable = require "../src/beautifiers/executable" path = require 'path' # Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. @@ -30,10 +31,15 @@ describe "PHP-CS-Fixer Beautifier", -> describe "Beautifier::beautify", -> beautifier = null + execSpawn = null beforeEach -> beautifier = new PHPCSFixer() # console.log('new beautifier') + execSpawn = Executable.prototype.spawn + + afterEach -> + Executable.prototype.spawn = execSpawn OSSpecificSpecs = -> text = "" @@ -49,13 +55,14 @@ describe "PHP-CS-Fixer Beautifier", -> levels: "" } # Mock spawn - beautifier.spawn = (exe, args, options) -> + # beautifier.spawn + Executable.prototype.spawn = (exe, args, options) -> # console.log('spawn', exe, args, options) er = new Error('ENOENT') er.code = 'ENOENT' return beautifier.Promise.reject(er) # Beautify - p = beautifier.beautify(text, language, options) + p = beautifier.loadExecutables().then(() -> beautifier.beautify(text, language, options)) expect(p).not.toBe(null) expect(p instanceof beautifier.Promise).toBe(true) cb = (v) -> @@ -74,7 +81,7 @@ describe "PHP-CS-Fixer Beautifier", -> expect(beautifier).not.toBe(null) expect(beautifier instanceof Beautifier).toBe(true) - if not beautifier.isWindows and failingProgram is "php" + if not Executable.isWindows and failingProgram is "php" # Only applicable on Windows return @@ -104,8 +111,9 @@ describe "PHP-CS-Fixer Beautifier", -> # console.log('fake exe path', exe) beautifier.Promise.resolve("/#{exe}") - oldSpawn = beautifier.spawn.bind(beautifier) - beautifier.spawn = (exe, args, options) -> + # oldSpawn = beautifier.spawn.bind(beautifier) + # beautifier.spawn + Executable.prototype.spawn = (exe, args, options) -> # console.log('spawn', exe, args, options) if exe is failingProgram er = new Error('ENOENT') @@ -117,21 +125,21 @@ describe "PHP-CS-Fixer Beautifier", -> stdout: 'stdout', stderr: '' }) - p = beautifier.beautify(text, language, options) + p = beautifier.loadExecutables().then(() -> beautifier.beautify(text, language, options)) expect(p).not.toBe(null) expect(p instanceof beautifier.Promise).toBe(true) p.then(cb, cb) return p - # failWhichProgram('php') - failWhichProgram('php-cs-fixer') + failWhichProgram('PHP') + # failWhichProgram('php-cs-fixer') unless isWindows describe "Mac/Linux", -> beforeEach -> # console.log('mac/linx') - beautifier.isWindows = false + Executable.isWindows = () -> false do OSSpecificSpecs @@ -139,6 +147,6 @@ describe "PHP-CS-Fixer Beautifier", -> beforeEach -> # console.log('windows') - beautifier.isWindows = true + Executable.isWindows = () -> true do OSSpecificSpecs diff --git a/src/beautifiers/beautifier.coffee b/src/beautifiers/beautifier.coffee index 5d326a3..3f2549d 100644 --- a/src/beautifiers/beautifier.coffee +++ b/src/beautifiers/beautifier.coffee @@ -4,8 +4,9 @@ fs = require('fs') temp = require('temp').track() readFile = Promise.promisify(fs.readFile) which = require('which') -spawn = require('child_process').spawn path = require('path') +shellEnv = require('shell-env') +Executable = require('./executable') module.exports = class Beautifier @@ -31,10 +32,42 @@ module.exports = class Beautifier ### options: {} + executables: [] + ### Is the beautifier a command-line interface beautifier? ### - isPreInstalled: true + isPreInstalled: () -> + @executables.length is 0 + + _exe: {} + loadExecutables: () -> + if Object.keys(@_exe).length is @executables.length + Promise.resolve(@_exe) + else + Promise.resolve(executables = @executables.map((e) -> new Executable(e))) + .then((executables) -> Promise.all(executables.map((e) -> e.init()))) + .then((es) => + exe = {} + missingInstalls = [] + es.forEach((e) -> + exe[e.cmd] = e + if not e.isInstalled + missingInstalls.push(e) + ) + @_exe = exe + @debug("exe", exe) + if missingInstalls.length is 0 + return @_exe + else + throw new Error("Missing required executables: #{missingInstalls.map((e) -> e.cmd).join(' and ')}") + ) + exe: (cmd) -> + console.log('exe', cmd, @_exe) + e = @_exe[cmd] + if !e? + throw new Error("Missing executable \"#{cmd}\". Please report this bug to https://github.com/Glavin001/atom-beautify/issues") + e ### Supported languages by this Beautifier @@ -102,7 +135,7 @@ module.exports = class Beautifier startDir.pop() return null -# Retrieves the default line ending based upon the Atom configuration + # Retrieves the default line ending based upon the Atom configuration # `line-ending-selector.defaultLineEnding`. If the Atom configuration # indicates "OS Default", the `process.platform` is queried, returning # CRLF for Windows systems and LF for all other systems. @@ -124,64 +157,6 @@ module.exports = class Beautifier else return lf - ### - If platform is Windows - ### - isWindows: do -> - return new RegExp('^win').test(process.platform) - - ### - Get Shell Environment variables - - Special thank you to @ioquatix - See https://github.com/ioquatix/script-runner/blob/v1.5.0/lib/script-runner.coffee#L45-L63 - ### - _envCache: null - _envCacheDate: null - _envCacheExpiry: 10000 # 10 seconds - getShellEnvironment: -> - return new Promise((resolve, reject) => - # Check Cache - if @_envCache? and @_envCacheDate? - # Check if Cache is old - if (new Date() - @_envCacheDate) < @_envCacheExpiry - # Still fresh - return resolve(@_envCache) - - # Check if Windows - if @isWindows - # Windows - # Use default - resolve(process.env) - else - # Mac & Linux - # I tried using ChildProcess.execFile but there is no way to set detached and - # this causes the child shell to lock up. - # This command runs an interactive login shell and - # executes the export command to get a list of environment variables. - # We then use these to run the script: - child = spawn process.env.SHELL, ['-ilc', 'env'], - # This is essential for interactive shells, otherwise it never finishes: - detached: true, - # We don't care about stdin, stderr can go out the usual way: - stdio: ['ignore', 'pipe', process.stderr] - # We buffer stdout: - buffer = '' - child.stdout.on 'data', (data) -> buffer += data - # When the process finishes, extract the environment variables and pass them to the callback: - child.on 'close', (code, signal) => - if code isnt 0 - return reject(new Error("Could not get Shell Environment. Exit code: "+code+", Signal: "+signal)) - environment = {} - for definition in buffer.split('\n') - [key, value] = definition.split('=', 2) - environment[key] = value if key != '' - # Cache Environment - @_envCache = environment - @_envCacheDate = new Date() - resolve(environment) - ) - ### Like the unix which utility. @@ -191,182 +166,19 @@ module.exports = class Beautifier See https://github.com/isaacs/node-which ### which: (exe, options = {}) -> - # Get PATH and other environment variables - @getShellEnvironment() - .then((env) => - new Promise((resolve, reject) => - options.path ?= env.PATH - if @isWindows - # Environment variables are case-insensitive in windows - # Check env for a case-insensitive 'path' variable - if !options.path - for i of env - if i.toLowerCase() is "path" - options.path = env[i] - break - - # Trick node-which into including files - # with no extension as executables. - # Put empty extension last to allow for other real extensions first - options.pathExt ?= "#{process.env.PATHEXT ? '.EXE'};" - which(exe, options, (err, path) -> - resolve(exe) if err - resolve(path) - ) - ) - ) - - ### - Add help to error.description - - Note: error.description is not officially used in JavaScript, - however it is used internally for Atom Beautify when displaying errors. - ### - commandNotFoundError: (exe, help) -> - # Create new improved error - # notify user that it may not be - # installed or in path - message = "Could not find '#{exe}'. \ - The program may not be installed." - er = new Error(message) - er.code = 'CommandNotFound' - er.errno = er.code - er.syscall = 'beautifier::run' - er.file = exe - if help? - if typeof help is "object" - # Basic notice - helpStr = "See #{help.link} for program \ - installation instructions.\n" - # Help to configure Atom Beautify for program's path - helpStr += "You can configure Atom Beautify \ - with the absolute path \ - to '#{help.program or exe}' by setting \ - '#{help.pathOption}' in \ - the Atom Beautify package settings.\n" if help.pathOption - # Optional, additional help - helpStr += help.additional if help.additional - # Common Help - issueSearchLink = - "https://github.com/Glavin001/atom-beautify/\ - search?q=#{exe}&type=Issues" - docsLink = "https://github.com/Glavin001/\ - atom-beautify/tree/master/docs" - helpStr += "Your program is properly installed if running \ - '#{if @isWindows then 'where.exe' \ - else 'which'} #{exe}' \ - in your #{if @isWindows then 'CMD prompt' \ - else 'Terminal'} \ - returns an absolute path to the executable. \ - If this does not work then you have not \ - installed the program correctly and so \ - Atom Beautify will not find the program. \ - Atom Beautify requires that the program be \ - found in your PATH environment variable. \n\ - Note that this is not an Atom Beautify issue \ - if beautification does not work and the above \ - command also does not work: this is expected \ - behaviour, since you have not properly installed \ - your program. Please properly setup the program \ - and search through existing Atom Beautify issues \ - before creating a new issue. \ - See #{issueSearchLink} for related Issues and \ - #{docsLink} for documentation. \ - If you are still unable to resolve this issue on \ - your own then please create a new issue and \ - ask for help.\n" - er.description = helpStr - else #if typeof help is "string" - er.description = help - return er + Executable.which(exe, options) ### Run command-line interface command ### run: (executable, args, {cwd, ignoreReturnCode, help, onStdin} = {}) -> - # Flatten args first - args = _.flatten(args) - - # Resolve executable and all args - Promise.all([executable, Promise.all(args)]) - .then(([exeName, args]) => - @debug('exeName, args:', exeName, args) - - # Get PATH and other environment variables - Promise.all([exeName, args, @getShellEnvironment(), @which(exeName)]) - ) - .then(([exeName, args, env, exePath]) => - @debug('exePath, env:', exePath, env) - @debug('args', args) - - exe = exePath ? exeName - options = { - cwd: cwd - env: env - } - - @spawn(exe, args, options, onStdin) - .then(({returnCode, stdout, stderr}) => - @verbose('spawn result', returnCode, stdout, stderr) - - # If return code is not 0 then error occured - if not ignoreReturnCode and returnCode isnt 0 - # operable program or batch file - windowsProgramNotFoundMsg = "is not recognized as an internal or external command" - - @verbose(stderr, windowsProgramNotFoundMsg) - - if @isWindows and returnCode is 1 and stderr.indexOf(windowsProgramNotFoundMsg) isnt -1 - throw @commandNotFoundError(exeName, help) - else - throw new Error(stderr) - else - stdout - ) - .catch((err) => - @debug('error', err) - - # Check if error is ENOENT (command could not be found) - if err.code is 'ENOENT' or err.errno is 'ENOENT' - throw @commandNotFoundError(exeName, help) - else - # continue as normal error - throw err - ) - ) - - ### - Spawn - ### - spawn: (exe, args, options, onStdin) -> - # Remove undefined/null values - args = _.without(args, undefined) - args = _.without(args, null) - - return new Promise((resolve, reject) => - @debug('spawn', exe, args) - - cmd = spawn(exe, args, options) - stdout = "" - stderr = "" - - cmd.stdout.on('data', (data) -> - stdout += data - ) - cmd.stderr.on('data', (data) -> - stderr += data - ) - cmd.on('close', (returnCode) => - @debug('spawn done', returnCode, stderr, stdout) - resolve({returnCode, stdout, stderr}) - ) - cmd.on('error', (err) => - @debug('error', err) - reject(err) - ) - - onStdin cmd.stdin if onStdin - ) + exe = new Executable({ + name: @name + homepage: @link + installation: @link + cmd: executable + }) + exe.run(args, {cwd, ignoreReturnCode, help, onStdin}) ### Logger instance diff --git a/src/beautifiers/executable.coffee b/src/beautifiers/executable.coffee new file mode 100644 index 0000000..ae8ef9b --- /dev/null +++ b/src/beautifiers/executable.coffee @@ -0,0 +1,309 @@ +Promise = require('bluebird') +_ = require('lodash') +which = require('which') +spawn = require('child_process').spawn +path = require('path') +semver = require('semver') +shellEnv = require('shell-env') + +module.exports = class Executable + + name = null + cmd = null + homepage = null + installation = null + versionArgs = ['--version'] + versionParse = (text) -> semver.clean(text) + versionsSupported: '>= 0.0.0' + + constructor: (options) -> + @name = options.name + @cmd = options.cmd + @homepage = options.homepage + @installation = options.installation + if options.version? + versionOptions = options.version + @versionArgs = versionOptions.args + @versionParse = versionOptions.parse + @versionsSupported = versionOptions.supported + @setupLogger() + + init: () -> + Promise.all([ + @loadVersion() + ]) + .then(() => @) + + ### + Logger instance + ### + logger: null + ### + Initialize and configure Logger + ### + setupLogger: -> + @logger = require('../logger')("#{@name} Executable") + for key, method of @logger + @[key] = method + @verbose("#{@name} executable logger has been initialized.") + + isInstalled = null + version = null + loadVersion: (force = false) -> + @verbose("loadVersion", @version, force) + if force or !@version? + @verbose("Loading version without cache") + @runVersion() + .then((text) => @versionParse(text)) + .then((version) -> + valid = Boolean(semver.valid(version)) + if not valid + throw new Error("Version is not valid: "+version) + version + ) + .then((version) => + @isInstalled = true + @version = version + ) + .then((version) => + @verbose("#{@cmd} version: #{version}") + version + ) + .catch((error) => + @isInstalled = false + @error(error) + Promise.reject(@commandNotFoundError()) + ) + else + @verbose("Loading cached version") + Promise.resolve(@version) + + runVersion: () -> + @run(@versionArgs) + + isSupported: () -> + @isVersion(@versionsSupported) + + isVersion: (range) -> + semver.satisfies(@version, range) + + ### + Run command-line interface command + ### + run: (args, options = {}) -> + @debug("Run: ", args, options) + exeName = @cmd + { cwd, ignoreReturnCode, help, onStdin } = options + # Flatten args first + args = _.flatten(args) + + # Resolve executable and all args + Promise.all([@shellEnv(), Promise.all(args)]) + .then(([env, args]) => + @debug('exeName, args:', exeName, args) + + # Get PATH and other environment variables + Promise.all([exeName, args, env, @which(exeName)]) + ) + .then(([exeName, args, env, exePath]) => + @debug('exePath:', exePath) + @debug('env:', env) + @debug('args', args) + + exe = exePath ? exeName + spawnOptions = { + cwd: cwd + env: env + } + + @spawn(exe, args, spawnOptions, onStdin) + .then(({returnCode, stdout, stderr}) => + @verbose('spawn result', returnCode, stdout, stderr) + + # If return code is not 0 then error occured + if not ignoreReturnCode and returnCode isnt 0 + # operable program or batch file + windowsProgramNotFoundMsg = "is not recognized as an internal or external command" + + @verbose(stderr, windowsProgramNotFoundMsg) + + if @isWindows() and returnCode is 1 and stderr.indexOf(windowsProgramNotFoundMsg) isnt -1 + throw @commandNotFoundError(exeName, help) + else + throw new Error(stderr) + else + stdout + ) + .catch((err) => + @debug('error', err) + + # Check if error is ENOENT (command could not be found) + if err.code is 'ENOENT' or err.errno is 'ENOENT' + throw @commandNotFoundError(exeName, help) + else + # continue as normal error + throw err + ) + ) + + ### + Spawn + ### + spawn: (exe, args, options, onStdin) -> + # Remove undefined/null values + args = _.without(args, undefined) + args = _.without(args, null) + + return new Promise((resolve, reject) => + @debug('spawn', exe, args) + + cmd = spawn(exe, args, options) + stdout = "" + stderr = "" + + cmd.stdout.on('data', (data) -> + stdout += data + ) + cmd.stderr.on('data', (data) -> + stderr += data + ) + cmd.on('close', (returnCode) => + @debug('spawn done', returnCode, stderr, stdout) + resolve({returnCode, stdout, stderr}) + ) + cmd.on('error', (err) => + @debug('error', err) + reject(err) + ) + + onStdin cmd.stdin if onStdin + ) + + + ### + Add help to error.description + + Note: error.description is not officially used in JavaScript, + however it is used internally for Atom Beautify when displaying errors. + ### + commandNotFoundError: (exe, help) -> + exe ?= @name or @cmd + # help ?= { + # program: @cmd + # link: @installation or @homepage + # } + # Create new improved error + # notify user that it may not be + # installed or in path + message = "Could not find '#{exe}'. \ + The program may not be installed." + er = new Error(message) + er.code = 'CommandNotFound' + er.errno = er.code + er.syscall = 'beautifier::run' + er.file = exe + if help? + if typeof help is "object" + # Basic notice + helpStr = "See #{help.link} for program \ + installation instructions.\n" + # Help to configure Atom Beautify for program's path + helpStr += "You can configure Atom Beautify \ + with the absolute path \ + to '#{help.program or exe}' by setting \ + '#{help.pathOption}' in \ + the Atom Beautify package settings.\n" if help.pathOption + # Optional, additional help + helpStr += help.additional if help.additional + # Common Help + issueSearchLink = + "https://github.com/Glavin001/atom-beautify/\ + search?q=#{exe}&type=Issues" + docsLink = "https://github.com/Glavin001/\ + atom-beautify/tree/master/docs" + helpStr += "Your program is properly installed if running \ + '#{if @isWindows() then 'where.exe' \ + else 'which'} #{exe}' \ + in your #{if @isWindows() then 'CMD prompt' \ + else 'Terminal'} \ + returns an absolute path to the executable. \ + If this does not work then you have not \ + installed the program correctly and so \ + Atom Beautify will not find the program. \ + Atom Beautify requires that the program be \ + found in your PATH environment variable. \n\ + Note that this is not an Atom Beautify issue \ + if beautification does not work and the above \ + command also does not work: this is expected \ + behaviour, since you have not properly installed \ + your program. Please properly setup the program \ + and search through existing Atom Beautify issues \ + before creating a new issue. \ + See #{issueSearchLink} for related Issues and \ + #{docsLink} for documentation. \ + If you are still unable to resolve this issue on \ + your own then please create a new issue and \ + ask for help.\n" + er.description = helpStr + else #if typeof help is "string" + er.description = help + return er + + + @_envCache = null + shellEnv: () -> + @constructor.shellEnv() + @shellEnv: () -> + if @_envCache + return Promise.resolve(@_envCache) + else + shellEnv() + .then((env) => + @_envCache = env + ) + + ### + Like the unix which utility. + + Finds the first instance of a specified executable in the PATH environment variable. + Does not cache the results, + so hash -r is not needed when the PATH changes. + See https://github.com/isaacs/node-which + ### + which: (exe, options) -> + @.constructor.which(exe, options) + @_whichCache = {} + @which: (exe, options = {}) -> + if @_whichCache[exe] + return Promise.resolve(@_whichCache[exe]) + # Get PATH and other environment variables + @shellEnv() + .then((env) => + new Promise((resolve, reject) => + options.path ?= env.PATH + if @isWindows() + # Environment variables are case-insensitive in windows + # Check env for a case-insensitive 'path' variable + if !options.path + for i of env + if i.toLowerCase() is "path" + options.path = env[i] + break + + # Trick node-which into including files + # with no extension as executables. + # Put empty extension last to allow for other real extensions first + options.pathExt ?= "#{process.env.PATHEXT ? '.EXE'};" + which(exe, options, (err, path) => + return resolve(exe) if err + @_whichCache[exe] = path + resolve(path) + ) + ) + ) + + ### + If platform is Windows + ### + isWindows: () -> @constructor.isWindows() + @isWindows: () -> new RegExp('^win').test(process.platform) diff --git a/src/beautifiers/gherkin.coffee b/src/beautifiers/gherkin.coffee index d17c897..2744818 100644 --- a/src/beautifiers/gherkin.coffee +++ b/src/beautifiers/gherkin.coffee @@ -3,8 +3,6 @@ "use strict" Beautifier = require('./beautifier') -Lexer = require('gherkin').Lexer('en') -logger = require('../logger')(__filename) module.exports = class Gherkin extends Beautifier name: "Gherkin formatter" @@ -15,6 +13,8 @@ module.exports = class Gherkin extends Beautifier } beautify: (text, language, options) -> + Lexer = require('gherkin').Lexer('en') + logger = @logger return new @Promise((resolve, reject) -> recorder = { lines: [] diff --git a/src/beautifiers/index.coffee b/src/beautifiers/index.coffee index bd22caf..6f212ab 100644 --- a/src/beautifiers/index.coffee +++ b/src/beautifiers/index.coffee @@ -345,36 +345,40 @@ module.exports = class Beautifiers extends EventEmitter filePath: filePath startTime = new Date() - beautifier.beautify(text, language.name, options, context) - .then((result) => - resolve(result) - # Track Timing - @trackTiming({ - utc: "Beautify" # Category - utv: language?.name # Variable - utt: (new Date() - startTime) # Value - utl: version # Label - }) - # Track Empty beautification results - if not result + beautifier.loadExecutables() + .then((executables) -> + logger.verbose('executables', executables) + beautifier.beautify(text, language.name, options, context) + ) + .then((result) => + resolve(result) + # Track Timing + @trackTiming({ + utc: "Beautify" # Category + utv: language?.name # Variable + utt: (new Date() - startTime) # Value + utl: version # Label + }) + # Track Empty beautification results + if not result + @trackEvent({ + ec: version, # Category + ea: "Beautify:Empty" # Action + el: language?.name # Label + }) + ) + .catch((error) => + reject(error) + # Track Errors @trackEvent({ ec: version, # Category - ea: "Beautify:Empty" # Action + ea: "Beautify:Error" # Action el: language?.name # Label }) - ) - .catch((error) => - reject(error) - # Track Errors - @trackEvent({ - ec: version, # Category - ea: "Beautify:Error" # Action - el: language?.name # Label - }) - ) - .finally(=> - @emit "beautify::end" - ) + ) + .finally(=> + @emit "beautify::end" + ) # Check if Analytics is enabled @trackEvent({ diff --git a/src/beautifiers/php-cs-fixer.coffee b/src/beautifiers/php-cs-fixer.coffee index db46818..d2129df 100644 --- a/src/beautifiers/php-cs-fixer.coffee +++ b/src/beautifiers/php-cs-fixer.coffee @@ -10,7 +10,30 @@ module.exports = class PHPCSFixer extends Beautifier name: 'PHP-CS-Fixer' link: "https://github.com/FriendsOfPHP/PHP-CS-Fixer" - isPreInstalled: false + executables: [ + { + name: "PHP" + cmd: "php" + homepage: "http://php.net/" + installation: "http://php.net/manual/en/install.php" + version: { + args: ['--version'] + parse: (text) -> text.match(/PHP (.*) \(cli\)/)[1] + supported: '>= 0.0.0' + } + } + { + name: "PHP-CS-Fixer" + cmd: "php-cs-fixer" + homepage: "https://github.com/FriendsOfPHP/PHP-CS-Fixer" + installation: "https://github.com/FriendsOfPHP/PHP-CS-Fixer#installation" + version: { + args: ['--version'] + parse: (text) -> text.match(/version (.*) by/)[1] + ".0" + supported: '>= 0.0.0' + } + } + ] options: PHP: @@ -24,7 +47,8 @@ module.exports = class PHPCSFixer extends Beautifier beautify: (text, language, options, context) -> @debug('php-cs-fixer', options) - version = options.cs_fixer_version + php = @exe('php') + phpCsFixer = @exe('php-cs-fixer') configFiles = ['.php_cs', '.php_cs.dist'] # Find a config file in the working directory if a custom one was not provided @@ -42,7 +66,7 @@ module.exports = class PHPCSFixer extends Beautifier "--allow-risky=#{options.allow_risky}" if options.allow_risky "--using-cache=no" ] - if version is 1 + if phpCsFixer.isVersion('1.x') phpCsFixerOptions = [ "fix" "--level=#{options.level}" if options.level @@ -60,7 +84,9 @@ module.exports = class PHPCSFixer extends Beautifier @Promise.all([ @which(options.cs_fixer_path) if options.cs_fixer_path @which('php-cs-fixer') - ]).then((paths) => + tempFile = @tempFile("temp", text, '.php') + ]).then(([customPath, phpCsFixerPath]) => + paths = [customPath, phpCsFixerPath] @debug('php-cs-fixer paths', paths) _ = require 'lodash' # Get first valid, absolute path @@ -71,10 +97,8 @@ module.exports = class PHPCSFixer extends Beautifier # Check if PHP-CS-Fixer path was found if phpCSFixerPath? # Found PHP-CS-Fixer path - tempFile = @tempFile("temp", text) - if @isWindows - @run("php", [phpCSFixerPath, phpCsFixerOptions, tempFile], runOptions) + php([phpCSFixerPath, phpCsFixerOptions, tempFile], runOptions) .then(=> @readFile(tempFile) ) diff --git a/src/beautifiers/phpcbf.coffee b/src/beautifiers/phpcbf.coffee index ad347b8..1cf2204 100644 --- a/src/beautifiers/phpcbf.coffee +++ b/src/beautifiers/phpcbf.coffee @@ -8,6 +8,17 @@ Beautifier = require('./beautifier') module.exports = class PHPCBF extends Beautifier name: "PHPCBF" link: "http://php.net/manual/en/install.php" + executables: [ + { + name: "PHPCBF" + cmd: "phpcbf" + homepage: "https://github.com/squizlabs/PHP_CodeSniffer" + installation: "https://github.com/squizlabs/PHP_CodeSniffer#installation" + version: { + args: ['--version'] + } + } + ] isPreInstalled: false options: {