diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..04ba039 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 2 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e7c1d93 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text eol=lf diff --git a/README.md b/README.md index 58ea599..4f9fe4a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,18 @@ # linter-flint -Lint projects using `flint` -You can find flint [here](https://github.com/pengwynn/flint). +Lint projects using [`flint`][flint]. + +# Installation + +You need to have [`flint`][flint] installed on your system before using this +package. Once that is setup this package should work right away. If you are +getting messages about `flint` not being available you may need to set the full +path to it in your settings. + +As this package is just a wrapper around `flint`, you will need something to +actually run it. By default the [Linter][] package will be installed for you to +fill this need. + + +[flint]: https://github.com/pengwynn/flint +[Linter]: https://atom.io/packages/linter diff --git a/lib/linter-flint.coffee b/lib/linter-flint.coffee deleted file mode 100644 index 6b8ccc1..0000000 --- a/lib/linter-flint.coffee +++ /dev/null @@ -1,39 +0,0 @@ -module.exports = LinterFlint = - config: - executablePath: - type: 'string' - default: 'flint' - skipReadme: - type: 'boolean' - default: false - skipContributing: - type: 'boolean' - default: false - skipLicense: - type: 'boolean' - default: false - skipBootstrap: - type: 'boolean' - default: false - skipTestScript: - type: 'boolean' - default: false - skipScripts: - type: 'boolean' - default: false - colorOutput: - type: 'boolean' - default: false - - activate: -> - require('atom-package-deps').install 'linter-flint' - - provideLinter: -> - LinterProvider = require('./provider') - @provider = new LinterProvider() - return { - grammarScopes: ['*'] - scope: 'project' - lint: @provider.lint - lintOnFly: true - } diff --git a/lib/linter-flint.js b/lib/linter-flint.js new file mode 100644 index 0000000..e0b01a4 --- /dev/null +++ b/lib/linter-flint.js @@ -0,0 +1,131 @@ +'use babel'; + +// eslint-disable-next-line import/no-extraneous-dependencies, import/extensions +import { CompositeDisposable } from 'atom'; +import { exec } from 'atom-linter'; +import { dirname } from 'path'; +import escapeHTML from 'escape-html'; + +// Internal vars +// Example: https://regex101.com/r/OfS9w0/2/ +const regex = /\[([A-Z]+)\] (.+)(?:\n\[INFO\] ([^\n.]+.)(?: (http:.+))?\n?)?/gm; + +export default { + activate() { + require('atom-package-deps').install('linter-flint'); + + this.subscriptions = new CompositeDisposable(); + + this.subscriptions.add( + atom.config.observe('linter-flint.executablePath', (value) => { + this.executablePath = value; + }), + ); + this.subscriptions.add( + atom.config.observe('linter-flint.skipReadme', (value) => { + this.skipReadme = value; + }), + ); + this.subscriptions.add( + atom.config.observe('linter-flint.skipContributing', (value) => { + this.skipContributing = value; + }), + ); + this.subscriptions.add( + atom.config.observe('linter-flint.skipLicense', (value) => { + this.skipLicense = value; + }), + ); + this.subscriptions.add( + atom.config.observe('linter-flint.skipBootstrap', (value) => { + this.skipBootstrap = value; + }), + ); + this.subscriptions.add( + atom.config.observe('linter-flint.skipTestScript', (value) => { + this.skipTestScript = value; + }), + ); + this.subscriptions.add( + atom.config.observe('linter-flint.skipScripts', (value) => { + this.skipScripts = value; + }), + ); + }, + + deactivate() { + this.subscriptions.dispose(); + }, + + provideLinter() { + return { + name: 'Flint', + grammarScopes: ['*'], + scope: 'project', + lintOnFly: false, + lint: async (editor) => { + const filePath = editor.getPath(); + let projectPath = atom.project.relativizePath(filePath)[0]; + if (projectPath === null) { + projectPath = dirname(filePath); + } + + const execArgs = ['--no-color']; + if (this.skipReadme) { + execArgs.push('--skip-readme'); + } + if (this.skipContributing) { + execArgs.push('--skip-contributing'); + } + if (this.skipLicense) { + execArgs.push('--skip-license'); + } + if (this.skipBootstrap) { + execArgs.push('--skip-bootstrap'); + } + if (this.skipTestScript) { + execArgs.push('--skip-test-script'); + } + if (this.skipScripts) { + execArgs.push('--skip-scripts'); + } + + const execOpts = { + cwd: projectPath, + stream: 'stderr', + }; + + const output = await exec(this.executablePath, execArgs, execOpts); + + const toReturn = []; + + let match = regex.exec(output); + while (match !== null) { + const [type, text, info, url] = match.slice(1); + if (type !== 'OK') { + const message = { + type: type === 'WARNING' ? 'Warning' : 'Error', + severity: type === 'WARNING' ? 'warning' : 'error', + text, + }; + if (info) { + const trace = { + type: 'Trace', + severity: 'info', + }; + if (url) { + trace.html = `${escapeHTML(info)} (link)`; + } else { + trace.text = info; + } + message.trace = [trace]; + } + toReturn.push(message); + } + match = regex.exec(output); + } + return toReturn; + }, + }; + }, +}; diff --git a/lib/provider.coffee b/lib/provider.coffee deleted file mode 100644 index 629b3f5..0000000 --- a/lib/provider.coffee +++ /dev/null @@ -1,44 +0,0 @@ -child_process = require 'child_process' -path = require 'path' - -module.exports = class LinterProvider - regex = /// - \[(\w+)\] # Type surrounded in square brackets. - \s(.*) # Message. - /// - - getCommand = -> - cmd = atom.config.get 'linter-flint.executablePath' - if atom.config.get 'linter-flint.skipReadme' - cmd = "#{cmd} --skip-readme" - if atom.config.get 'linter-flint.skipContributing' - cmd = "#{cmd} --skip-contributing" - if atom.config.get 'linter-flint.skipLicense' - cmd = "#{cmd} --skip-license" - if atom.config.get 'linter-flint.skipBootstrap' - cmd = "#{cmd} --skip-bootstrap" - if atom.config.get 'linter-flint.skipTestScript' - cmd = "#{cmd} --skip-test-script" - if atom.config.get 'linter-flint.skipScripts' - cmd = "#{cmd} --skip-scripts" - unless atom.config.get 'linter-flint.colorOutput' - cmd = "#{cmd} --no-color" - return cmd - - lint: -> - return new Promise (resolve) -> - projectPath = atom.project.getPaths()[0] - data = '' - process = child_process.exec(getCommand(), {cwd: projectPath}) - process.stderr.on 'data', (d) -> data = d.toString() - process.on 'close', -> - toReturn = [] - for line in data.split('\n') - #console.log "Flint Provider: #{line}" if atom.inDevMode() - if line.match regex - [type, message] = line.match(regex)[1..2] - toReturn.push( - type: type, - text: message - ) - resolve toReturn diff --git a/package.json b/package.json index d18fc48..3d034b6 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,42 @@ { "name": "linter-flint", - "main": "./lib/linter-flint", + "main": "./lib/linter-flint.js", "version": "0.1.2", "description": "Lint projects using flint", "repository": "https://github.com/AtomLinter/linter-flint", "license": "MIT", "engines": { - "atom": ">0.50.0" + "atom": ">=1.4.0 <2.0.0" + }, + "configSchema": { + "executablePath": { + "type": "string", + "default": "flint" + }, + "skipReadme": { + "type": "boolean", + "default": false + }, + "skipContributing": { + "type": "boolean", + "default": false + }, + "skipLicense": { + "type": "boolean", + "default": false + }, + "skipBootstrap": { + "type": "boolean", + "default": false + }, + "skipTestScript": { + "type": "boolean", + "default": false + }, + "skipScripts": { + "type": "boolean", + "default": false + } }, "providedServices": { "linter": { @@ -16,7 +46,34 @@ } }, "dependencies": { - "atom-package-deps": "^4.0.1" + "atom-linter": "^9.0.1", + "atom-package-deps": "^4.0.1", + "escape-html": "^1.0.3" + }, + "devDependencies": { + "eslint": "^3.16.1", + "eslint-config-airbnb-base": "^11.1.1", + "eslint-plugin-import": "^2.2.0" + }, + "eslintConfig": { + "extends": "airbnb-base", + "rules": { + "global-require": "off", + "import/no-unresolved": [ + "error", + { + "ignore": [ + "atom" + ] + } + ] + }, + "globals": { + "atom": true + }, + "env": { + "node": true + } }, "package-deps": [ "linter" diff --git a/spec/.eslintrc.js b/spec/.eslintrc.js new file mode 100644 index 0000000..fee793a --- /dev/null +++ b/spec/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + env: { + atomtest: true, + jasmine: true + } +}; diff --git a/spec/fixtures/badProj/CHANGELOG.md b/spec/fixtures/badProj/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/badProj/CODE_OF_CONDUCT.md b/spec/fixtures/badProj/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/badProj/CONTRIBUTING.md b/spec/fixtures/badProj/CONTRIBUTING.md new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/badProj/LICENSE.md b/spec/fixtures/badProj/LICENSE.md new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/badProj/README.md b/spec/fixtures/badProj/README.md new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/badProj/package.json b/spec/fixtures/badProj/package.json new file mode 100644 index 0000000..6be3ed4 --- /dev/null +++ b/spec/fixtures/badProj/package.json @@ -0,0 +1,3 @@ +{ + "name": "invalid project" +} diff --git a/spec/fixtures/goodProj/CHANGELOG.md b/spec/fixtures/goodProj/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/goodProj/CODE_OF_CONDUCT.md b/spec/fixtures/goodProj/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/goodProj/CONTRIBUTING.md b/spec/fixtures/goodProj/CONTRIBUTING.md new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/goodProj/LICENSE.md b/spec/fixtures/goodProj/LICENSE.md new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/goodProj/README.md b/spec/fixtures/goodProj/README.md new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/goodProj/package.json b/spec/fixtures/goodProj/package.json new file mode 100644 index 0000000..6be3ed4 --- /dev/null +++ b/spec/fixtures/goodProj/package.json @@ -0,0 +1,3 @@ +{ + "name": "invalid project" +} diff --git a/spec/fixtures/goodProj/script/bootstrap b/spec/fixtures/goodProj/script/bootstrap new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/goodProj/script/test b/spec/fixtures/goodProj/script/test new file mode 100644 index 0000000..e69de29 diff --git a/spec/linter-flint-spec.js b/spec/linter-flint-spec.js new file mode 100644 index 0000000..9f6a128 --- /dev/null +++ b/spec/linter-flint-spec.js @@ -0,0 +1,60 @@ +'use babel'; + +import * as path from 'path'; + +const lint = require('../lib/linter-flint.js').provideLinter().lint; + +const goodPath = path.join(__dirname, 'fixtures', 'goodProj', 'package.json'); +const badPath = path.join(__dirname, 'fixtures', 'badProj', 'package.json'); + +describe('The Flint provider for Linter', () => { + beforeEach(() => { + // Close any open file + atom.workspace.destroyActivePaneItem(); + // Close all project paths + const projectPaths = atom.project.getPaths(); + projectPaths.forEach(projectPath => atom.project.removePath(projectPath)); + // Activate the linter + waitsForPromise(() => atom.packages.activatePackage('linter-flint')); + }); + + it('checks a project with no issues and finds nothing wrong', () => { + atom.project.addPath(path.dirname(goodPath)); + waitsForPromise(() => atom.workspace.open(goodPath).then(editor => + lint(editor).then(messages => + expect(messages.length).toBe(0), + ), + )); + }); + + it('checks a project with issues and reports the found issues', () => { + atom.project.addPath(path.dirname(badPath)); + waitsForPromise(() => atom.workspace.open(badPath).then(editor => + lint(editor).then((messages) => { + expect(messages[0].type).toBe('Warning'); + expect(messages[0].severity).toBe('warning'); + expect(messages[0].html).not.toBeDefined(); + expect(messages[0].text).toBe('Bootstrap script not found'); + expect(messages[0].filePath).not.toBeDefined(); + expect(messages[0].range).not.toBeDefined(); + expect(messages[0].trace).toEqual([{ + type: 'Trace', + severity: 'info', + html: 'A bootstrap script makes setup a snap. (link)', + }]); + + expect(messages[1].type).toBe('Warning'); + expect(messages[1].severity).toBe('warning'); + expect(messages[1].html).not.toBeDefined(); + expect(messages[1].text).toBe('Test script not found'); + expect(messages[1].filePath).not.toBeDefined(); + expect(messages[1].range).not.toBeDefined(); + expect(messages[1].trace).toEqual([{ + type: 'Trace', + severity: 'info', + html: 'Make it easy to run the test suite regardless of project type. (link)', + }]); + }), + )); + }); +});