├── .editorconfig ├── .eslintignore ├── .flowconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── declarations └── modules.js ├── lib ├── helpers.js ├── index.js ├── message.js └── types.js ├── package.json └── spec ├── .eslintrc.js ├── linter-spec.js └── test ├── arrays ├── .flowconfig └── Arrays.js └── constructor /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | spec/test/ 2 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/sb-promisify/.* 3 | .*/spec/.* 4 | 5 | [include] 6 | 7 | [libs] 8 | ./declarations/modules.js 9 | 10 | [options] 11 | unsafe.enable_getters_and_setters=true 12 | esproposal.decorators=ignore 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | ### Project specific config ### 2 | language: node_js 3 | 4 | matrix: 5 | include: 6 | - os: linux 7 | node_js: "6" 8 | env: ATOM_CHANNEL=stable 9 | 10 | - os: linux 11 | node_js: "node" 12 | env: ATOM_CHANNEL=beta 13 | 14 | install: 15 | - npm install -g flow-bin 16 | 17 | # The linting done in the Atom script doesn't handle flow 18 | after_script: 19 | - npm run lint 20 | 21 | ### Generic setup follows ### 22 | script: 23 | - curl -s -O https://raw.githubusercontent.com/atom/ci/master/build-package.sh 24 | - chmod u+x build-package.sh 25 | - ./build-package.sh 26 | 27 | notifications: 28 | email: 29 | on_success: never 30 | on_failure: change 31 | 32 | branches: 33 | only: 34 | - master 35 | - /^greenkeeper/.*$/ 36 | 37 | git: 38 | depth: 10 39 | 40 | sudo: false 41 | 42 | dist: trusty 43 | 44 | addons: 45 | apt: 46 | packages: 47 | - build-essential 48 | - fakeroot 49 | - git 50 | - libsecret-1-dev 51 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v5.6.1 4 | 5 | * Update `flow-bin` to v0.53.1 6 | * Update dependencies 7 | 8 | ## v5.6.0 9 | 10 | * Update dependencies 11 | * Update `flow-bin` to v0.44.0 12 | 13 | ## v5.5.1 14 | 15 | * Update dependencies 16 | 17 | ## v5.5.0 18 | 19 | * Update `flow-bin` to v0.30.0. 20 | 21 | ## v5.1.0 22 | 23 | * Reverted the flow comment detection as the new implementation was buggy. 24 | 25 | * Throttle the number of atom notifications, and limit console logs to dev 26 | mode. 27 | 28 | ## v5.0.0 29 | 30 | **Bug Fixes** 31 | 32 | * Removed the flow server management as it was causing problems. 33 | * Instead do a `flow stop` on deactivate. 34 | * Improved type checking using flow of the code base. 35 | 36 | **Enhancements** 37 | 38 | * Change to a more strict check for a flow comment, to reduce false-positives 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Naman Goel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # linter-flow 2 | 3 | ## HELP WANTED! 4 | 5 | I'm looking for contributors to help maintain this plug-in. I don't have the time I used to be able to keep this up-to-date, but I hate to let this languish. 6 | 7 | Lightweight alternative to Facebook's Flow plugin for [facebook/flow](http://flowtype.org/). 8 | 9 | ![linter-demo](https://naman.s3.amazonaws.com/linter-flow-plus/linter-flow-plus.gif) 10 | 11 | ## Installation 12 | 13 | * Install [flow](http://flowtype.org/docs/getting-started.html#installing-flow) 14 | * `flow init` 15 | * `apm install linter-flow` 16 | 17 | ## Settings 18 | 19 | You can configure linter-flow by editing ~/.atom/config.cson (choose Open Your Config in Atom menu) or in Preferences: 20 | 21 | ```cson 22 | 'linter-flow': 23 | 'executablePath': 'flow' 24 | 'enableAll': false 25 | ``` 26 | 27 | * `executablePath`: Absolute path to the Flow executable on your system. 28 | * `enableAll`: Typecheck all files, not just file containing `@flow`. 29 | 30 | ### Why not X? 31 | 32 | linter-flow is made to be a lightweight package that does one thing well. 33 | 34 | 1. A similarly named package: linter-flow-plus is now a mirror. The development happens for both packages in parallel. 35 | 2. IDE-flow works relatively well, but it doesn't lint on-the-fly and doesn't integrate with the linter package. 36 | 3. Nuclide has too many problems for now to be reliable. It also involves installing a large number of other packages. 37 | 38 | Please Note: IDE-flow and Nuclide provide other features such as autocomplete, type definitions on hover etc. Please continue to use those services for those features. (possibly in addition to linter-flow) 39 | 40 | ### Limitations 41 | 42 | This linter currently does not support Hack. Though the linter just uses the flow-cli and hack support should be trivial to add, I'm not a Hack/PHP developer and I can't test that it actually works. I would welcome if someone was to add support for Hack to this package and test it. 43 | 44 | ## Contributing 45 | 46 | If you would like to contribute enhancements or fixes, please do the following: 47 | 48 | 1. Fork the plugin repository 49 | 2. Hack on a separate topic branch created from the latest `master` 50 | 3. Commit and push the topic branch 51 | 4. Make a pull request 52 | 5. Welcome to the club! 53 | 54 | Please note that modifications should follow these coding guidelines: 55 | 56 | * Indent is 2 spaces with `.editorconfig` 57 | * Code should pass `eslint` linter with the provided `.eslintrc` 58 | * Vertical whitespace helps readability, don’t be afraid to use it 59 | 60 | **Thank you for helping out!** 61 | -------------------------------------------------------------------------------- /declarations/modules.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* @flow */ 3 | 4 | declare var atom: Object; 5 | 6 | declare class Subscription { 7 | dispose(): void; 8 | } 9 | 10 | declare class Config { 11 | observe(config: string, cb: Function): Subscription; 12 | } 13 | 14 | declare class CompositeDisposableClass { 15 | add(observable: Subscription): void; 16 | dispose(): void; 17 | } 18 | 19 | declare module 'atom' { 20 | declare var Range: any; 21 | declare var CompositeDisposable: any; 22 | declare var config: Config; 23 | } 24 | 25 | declare module 'atom-package-deps' { 26 | declare function install(packageName: string): void; 27 | } 28 | 29 | declare module 'atom-linter' { 30 | declare function find(filePath: string, fileName: string): ?string; 31 | declare function exec(executable: string, args?: Array, config?: Object): Promise; 32 | } 33 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | 'use babel'; 4 | 5 | const helpers = require('atom-linter'); 6 | 7 | export default function check( 8 | pathToFlow: string, 9 | args: Array, 10 | options: Object, 11 | startTime: number = Date.now(), 12 | ): Promise { 13 | return helpers 14 | .exec(pathToFlow, args, options) 15 | .catch((error: string | Object) => { 16 | const errorM: string = String(error).toLowerCase(); 17 | 18 | // If we'be been waiting for more than 10 seconds, just give up 19 | if (Date.now() - startTime > 10000) { 20 | return '[]'; 21 | } 22 | // Check for the common flow status messages and ignore them 23 | if (errorM.indexOf('rechecking') !== -1 || 24 | errorM.indexOf('launching') !== -1 || 25 | errorM.indexOf('processing') !== -1 || 26 | errorM.indexOf('starting') !== -1 || 27 | errorM.indexOf('spawned') !== -1 || 28 | errorM.indexOf('logs') !== -1 || 29 | errorM.indexOf('initializing') !== -1 30 | ) { 31 | return check(pathToFlow, args, options); 32 | } 33 | throw error; 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // eslint-disable-next-line import/extensions 4 | import { CompositeDisposable } from 'atom'; 5 | import path from 'path'; 6 | 7 | import handleData from './message'; 8 | import type { Linter } from './types'; 9 | import check from './helpers'; 10 | 11 | export default { 12 | config: { 13 | executablePath: { 14 | type: 'string', 15 | default: 'flow', 16 | description: 'Absolute path to the Flow executable on your system.', 17 | }, 18 | }, 19 | 20 | activate(): void { 21 | require('atom-package-deps').install('linter-flow'); 22 | 23 | this.lastConfigError = {}; 24 | this.flowInstances = new Set(); 25 | 26 | this.subscriptions = new CompositeDisposable(); 27 | this.subscriptions.add(atom.config.observe('linter-flow.executablePath', (pathToFlow) => { 28 | this.pathToFlow = pathToFlow; 29 | })); 30 | }, 31 | 32 | deactivate(): void { 33 | const helpers = require('atom-linter'); 34 | 35 | if (atom.inDevMode()) { 36 | console.log('linter-flow:: Stopping flow...'); 37 | } 38 | this.flowInstances.forEach(cwd => 39 | helpers.exec(this.pathToFlow, ['stop'], { cwd }).catch(() => null)); 40 | this.subscriptions.dispose(); 41 | }, 42 | 43 | provideLinter(): Linter { 44 | const helpers = require('atom-linter'); 45 | 46 | return { 47 | grammarScopes: [ 48 | 'source.js', 49 | 'source.js.jsx', 50 | 'source.babel', 51 | 'source.js-semantic', 52 | 'source.es6', 53 | ], 54 | scope: 'project', 55 | name: 'Flow', 56 | lintOnFly: true, 57 | lint: (TextEditor) => { 58 | const filePath = TextEditor.getPath(); 59 | const fileText = TextEditor.getText(); 60 | 61 | // Is flow enabled for current file? 62 | if (!fileText || fileText.indexOf('@flow') === -1) { 63 | return Promise.resolve([]); 64 | } 65 | 66 | // Check if .flowconfig file is present 67 | const flowConfig = helpers.find(filePath, '.flowconfig'); 68 | if (!flowConfig) { 69 | // Only warn every 5 min 70 | if (!this.lastConfigError[filePath] || 71 | this.lastConfigError[filePath] + (5 * 60 * 1000) < Date.now()) { 72 | atom.notifications.addWarning('[Linter-Flow] Missing .flowconfig file.', { 73 | detail: 'To get started with Flow, run `flow init`.', 74 | dismissable: true, 75 | }); 76 | this.lastConfigError[filePath] = Date.now(); 77 | } 78 | return Promise.resolve([]); 79 | } else if (Object.hasOwnProperty.call(this.lastConfigError, filePath)) { 80 | delete this.lastConfigError[filePath]; 81 | } 82 | 83 | let args; 84 | let options; 85 | 86 | const cwd = path.dirname(flowConfig); 87 | this.flowInstances.add(cwd); 88 | // Use `check-contents` for unsaved files, and `status` for saved files. 89 | if (TextEditor.isModified()) { 90 | args = ['check-contents', '--json', '--root', cwd, filePath]; 91 | options = { cwd, stdin: fileText, ignoreExitCode: true }; 92 | } else { 93 | args = ['status', '--json', filePath]; 94 | options = { cwd, ignoreExitCode: true }; 95 | } 96 | 97 | return check(this.pathToFlow, args, options) 98 | .then(JSON.parse) 99 | .then(handleData); 100 | }, 101 | }; 102 | }, 103 | }; 104 | -------------------------------------------------------------------------------- /lib/message.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // eslint-disable-next-line import/extensions 4 | import { Range } from 'atom'; 5 | 6 | import type { FlowError, FlowMessage, LinterTrace, LinterMessageV1 } from './types'; 7 | 8 | function extractRange(message: FlowMessage): Range { 9 | return new Range( 10 | [message.line - 1, message.start - 1], 11 | [message.endline - 1, message.end], 12 | ); 13 | } 14 | 15 | function flowMessageToTrace(message: FlowMessage): LinterTrace { 16 | return { 17 | type: 'Trace', 18 | text: message.descr, 19 | filePath: message.path, 20 | range: extractRange(message), 21 | }; 22 | } 23 | 24 | function flowErrorToLinterMessages(flowError: FlowError): Array { 25 | const blameMessages = flowError.message.filter((m: FlowMessage) => m.type === 'Blame'); 26 | 27 | return blameMessages.map((message: FlowMessage, i) => ({ 28 | type: flowError.level === 'error' ? 'Error' : 'Warning', 29 | text: flowError.message.map((msg: FlowMessage) => msg.descr).join(' '), 30 | filePath: message.path || null, 31 | range: extractRange(message), 32 | trace: [...blameMessages.slice(0, i), ...blameMessages.slice(i + 1)].map(flowMessageToTrace), 33 | })); 34 | } 35 | 36 | function handleData(json: any): Array { 37 | if (json.passed || !json.errors) { 38 | return []; 39 | } 40 | return json.errors.reduce((messages, error) => 41 | messages.concat(flowErrorToLinterMessages(error)), []); 42 | } 43 | 44 | export default handleData; 45 | -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/extensions 2 | import { Range } from 'atom'; 3 | 4 | export type LinterTrace = { 5 | type: 'Trace'; 6 | text?: string; 7 | html?: string; 8 | filePath: string; 9 | range?: Range; 10 | }; 11 | 12 | export type LinterMessageV1 = { 13 | type: 'Error' | 'Warning'; 14 | text?: string; 15 | html?: string; 16 | filePath?: string; 17 | range?: Range; 18 | trace?: Array; 19 | }; 20 | 21 | export type FlowPoint = { 22 | column: number; 23 | line: number; 24 | offset: number; 25 | } 26 | 27 | export type FlowLocation = { 28 | end: FlowPoint; 29 | source: string; 30 | start: FlowPoint; 31 | } 32 | 33 | export type FlowMessage = { 34 | context: string; 35 | descr: string; 36 | end: number; 37 | endline: number; 38 | line: number; 39 | loc?: FlowLocation; 40 | path: string; 41 | start: number; 42 | type: string; 43 | }; 44 | 45 | export type FlowError = { 46 | kind: string; 47 | level: string; 48 | message: Array; 49 | } 50 | 51 | export type BufferType = { 52 | cachedText: string; 53 | getText(): string; 54 | }; 55 | 56 | export type TextEditorType = { 57 | getPath(): string; 58 | getText(): string; 59 | buffer: BufferType; 60 | isModified(): boolean; 61 | }; 62 | 63 | export type Linter = { 64 | grammarScopes: Array; 65 | scope: 'file' | 'project'; 66 | name?: string; 67 | lintOnFly: boolean; 68 | lint(TextEditor: TextEditorType): Array | Promise>; 69 | }; 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linter-flow", 3 | "version": "5.6.1", 4 | "description": "Lint JavaScript on the fly, using Flow", 5 | "repository": "https://github.com/AtomLinter/linter-flow.git", 6 | "homepage": "https://github.com/AtomLinter/linter-flow", 7 | "author": "Naman Goel", 8 | "license": "MIT", 9 | "engines": { 10 | "atom": ">=1.0.0 <2.0.0" 11 | }, 12 | "main": "./lib/index.js", 13 | "scripts": { 14 | "lint": "eslint . && flow check", 15 | "test": "apm test" 16 | }, 17 | "dependencies": { 18 | "atom-linter": "^10.0.0", 19 | "atom-package-deps": "^4.0.1" 20 | }, 21 | "package-deps": [ 22 | "linter" 23 | ], 24 | "providedServices": { 25 | "linter": { 26 | "versions": { 27 | "1.0.0": "provideLinter" 28 | } 29 | } 30 | }, 31 | "devDependencies": { 32 | "babel-eslint": "^8.0.0", 33 | "eslint": "^4.6.0", 34 | "eslint-config-airbnb-base": "^12.0.0", 35 | "eslint-plugin-import": "^2.7.0", 36 | "flow-bin": "^0.56.0", 37 | "jasmine-fix": "^1.3.1" 38 | }, 39 | "eslintConfig": { 40 | "extends": "airbnb-base", 41 | "parser": "babel-eslint", 42 | "env": { 43 | "es6": true, 44 | "node": true 45 | }, 46 | "globals": { 47 | "atom": true 48 | }, 49 | "rules": { 50 | "no-console": "off", 51 | "global-require": "off", 52 | "import/no-extraneous-dependencies": "off", 53 | "import/no-unresolved": [ 54 | "error", 55 | { 56 | "ignore": [ 57 | "atom" 58 | ] 59 | } 60 | ] 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /spec/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jasmine: true, 4 | atomtest: true 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /spec/linter-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import * as path from 'path'; 4 | // eslint-disable-next-line no-unused-vars 5 | import { it, fit, wait, beforeEach, afterEach } from 'jasmine-fix'; 6 | 7 | const { lint } = require('../lib/index.js').provideLinter(); 8 | 9 | const constructorPath = path.join(__dirname, 'test', 'constructor', 'constructor.js'); 10 | const arrayPath = path.join(__dirname, 'test', 'arrays', 'Arrays.js'); 11 | 12 | describe('Flow provider for Linter', () => { 13 | beforeEach(async () => { 14 | waitsForPromise(() => 15 | atom.packages.activatePackage('linter-flow')); 16 | /** 17 | * Note: Windows seems unable to use a globally installed version, if 18 | * testing locally, fix the path below and uncomment the line to get the 19 | * specs to work for you. Make sure you restore any changes before 20 | * committing. 21 | */ 22 | // atom.config.set('linter-flow.executablePath', 'C:\\path\\to\\flow.cmd'); 23 | }); 24 | 25 | it('constructor: incompatible type', async () => { 26 | const editor = await atom.workspace.open(constructorPath); 27 | const messages = await lint(editor); 28 | const msgText = 'number This type is incompatible with an implicitly-returned undefined.'; 29 | 30 | expect(messages.length).toBe(1); 31 | expect(messages[0].type).toBe('Error'); 32 | expect(messages[0].text).toBe(msgText); 33 | expect(messages[0].filePath).toBe(constructorPath); 34 | expect(messages[0].trace.length).toBe(0); 35 | expect(messages[0].range).toEqual([[6, 18], [6, 24]]); 36 | }); 37 | 38 | it('arrays: incompatible type', async () => { 39 | const editor = await atom.workspace.open(arrayPath); 40 | const messages = await lint(editor); 41 | const msgText = 'number This type is incompatible with the expected param type of string'; 42 | 43 | expect(messages.length).toBe(2); 44 | 45 | expect(messages[0].type).toBe('Error'); 46 | expect(messages[0].text).toBe(msgText); 47 | expect(messages[0].filePath).toBe(arrayPath); 48 | expect(messages[0].trace.length).toBe(1); 49 | expect(messages[0].trace[0].range).toEqual([[3, 16], [3, 22]]); 50 | expect(messages[0].range).toEqual([[9, 4], [9, 8]]); 51 | 52 | expect(messages[1].type).toBe('Error'); 53 | expect(messages[1].text).toBe(msgText); 54 | expect(messages[1].filePath).toBe(arrayPath); 55 | expect(messages[1].trace.length).toBe(1); 56 | expect(messages[1].trace[0].range).toEqual([[9, 4], [9, 8]]); 57 | expect(messages[1].range).toEqual([[3, 16], [3, 22]]); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /spec/test/arrays/.flowconfig: -------------------------------------------------------------------------------- 1 | [options] 2 | module.system=haste 3 | 4 | -------------------------------------------------------------------------------- /spec/test/arrays/Arrays.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* @providesModule Arrays */ 3 | 4 | function foo(x: string) { } 5 | 6 | var a = []; 7 | a[0] = 1; 8 | a[1] = '...'; 9 | 10 | foo(a[1]); 11 | var y; 12 | a.forEach(x => y=x); 13 | 14 | // for literals, composite element type is union of individuals 15 | // note: test both tuple and non-tuple inferred literals 16 | var alittle: Array = [0, 1, 2, 3, null]; 17 | var abig: Array = [0, 1, 2, 3, 4, 5, 6, 8, null]; 18 | 19 | var abig2: Array<{x:number; y:number}> = [ 20 | {x:0, y:0}, 21 | {x:0, y:0}, 22 | {x:0, y:0}, 23 | {x:0, y:0}, 24 | {x:0, y:0}, 25 | {x:0, y:0}, 26 | {x:0, y:0}, 27 | {x:0, y:0}, 28 | {x:0, y:0}, 29 | {x:0, y:0}, 30 | {x:0, y:0}, 31 | {x:0, y:0}, 32 | {x:0, y:0}, 33 | {x:0, y:0, a:true}, 34 | {x:0, y:0, b:"hey"}, 35 | {x:0, y:0, c:1}, 36 | {x:0, y:0, c:"hey"} 37 | ]; 38 | 39 | module.exports = "arrays"; 40 | -------------------------------------------------------------------------------- /spec/test/constructor/.flowconfig: -------------------------------------------------------------------------------- 1 | [options] 2 | module.system=haste 3 | 4 | -------------------------------------------------------------------------------- /spec/test/constructor/constructor.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | class C { 3 | constructor() { } 4 | } 5 | 6 | class D { 7 | constructor():number { } 8 | } 9 | 10 | module.exports = C; 11 | --------------------------------------------------------------------------------