diff --git a/.codeintel/config b/.codeintel/config new file mode 100644 index 00000000..cadb2ed1 --- /dev/null +++ b/.codeintel/config @@ -0,0 +1,9 @@ +{ + "Python": { + "pythonExtraPaths": [ + "libs", + "~/Applications/Sublime Text 2.app/Contents/MacOS", + "/Applications/Sublime Text 2.app/Contents/MacOS" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6c812814 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.git +.hg +.svn + +*.pyc +*.pyo + diff --git a/Base File.sublime-settings b/Base File.sublime-settings new file mode 100644 index 00000000..d662993b --- /dev/null +++ b/Base File.sublime-settings @@ -0,0 +1,117 @@ +/* + SublimeLinter default settings +*/ +{ + /* + Sets the mode in which SublimeLinter runs: + + true - Linting occurs in the background as you type (the default). + false - Linting only occurs when you initiate it. + "load-save" - Linting occurs only when a file is loaded and saved. + */ + "sublimelinter": true, + + /* + Maps linters to executables for non-built in linters. If the executable + is not in the default system path, or on posix systems in /usr/local/bin + or ~/bin, then you must specify the full path to the executable. + Linter names should be lowercase. + + This is the effective default map; your mappings may override these. + + "sublimelinter_executable_map": + { + "perl": "perl", + "php": "php", + "ruby": "ruby" + }, + */ + "sublimelinter_executable_map": + { + }, + + /* + Maps syntax names to linters. This allows variations on a syntax + (for example "Python (Django)") to be linted. The key is + the base filename of the .tmLanguage syntax files, and the value + is the linter name (lowercase) the syntax maps to. + */ + "sublimelinter_syntax_map": + { + "Python Django": "python" + }, + + // An array of linter names to disable. Names should be lowercase. + "sublimelinter_disable": + [ + ], + + /* + The minimum delay in seconds (fractional seconds are okay) before + a linter is run when the "sublimelinter" setting is true. This allows + you to have background linting active, but defer the actual linting + until you are idle. When this value is greater than the built in linting delay, + errors are erased when the file is modified, since the assumption is + you don't want to see errors while you type. + */ + "sublimelinter_delay": 0, + + // If true, lines with errors or warnings will be filled in with the outline color. + "sublimelinter_fill_outlines": false, + + // If true, lines with errors or warnings will have a gutter mark. + "sublimelinter_gutter_marks": false, + + // If true, the find next/previous error commands will wrap. + "sublimelinter_wrap_find": true, + + // If true, when the file is saved any errors will appear in a popup list + "sublimelinter_popup_errors_on_save": false, + + // jshint: options for linting JavaScript. See http://jshint.com/#docs for more info. + // By deault, eval is allowed. + "jshint_options": + { + "evil": true, + "regexdash": true, + "browser": true, + "wsh": true, + "trailing": true, + "sub": true + }, + + // A list of pep8 error numbers to ignore. By default "line too long" errors are ignored. + // The list of error codes is in this file: https://github.com/jcrocholl/pep8/blob/master/pep8.py. + // Search for "Ennn:", where nnn is a 3-digit number. + "pep8_ignore": + [ + "E501" + ], + + /* + If you use SublimeLinter for pyflakes checks, you can ignore some of the "undefined name xxx" + errors (comes in handy if you work with post-processors, globals/builtins available only at runtime, etc.). + You can control what names will be ignored with the user setting "pyflakes_ignore". + + Example: + + "pyflakes_ignore": + [ + "some_custom_builtin_o_mine", + "A_GLOBAL_CONSTANT" + ], + */ + "pyflakes_ignore": + [ + ], + + /* + Ordinarily pyflakes will issue a warning when 'from foo import *' is used, + but it is ignored since the warning is not that helpful. If you want to see this warning, + set this option to false. + */ + "pyflakes_ignore_import_*": true, + + // Objective-J: if true, non-ascii characters are flagged as an error. + "sublimelinter_objj_check_ascii": false +} diff --git a/Default (Linux).sublime-keymap b/Default (Linux).sublime-keymap new file mode 100644 index 00000000..c7c19ede --- /dev/null +++ b/Default (Linux).sublime-keymap @@ -0,0 +1,5 @@ +[ + { "keys": ["ctrl+alt+l"], "command": "sublimelinter", "args": {"action": "lint"} }, + { "keys": ["ctrl+alt+e"], "command": "find_next_lint_error" }, + { "keys": ["ctrl+alt+shift+e"], "command": "find_previous_lint_error" } +] diff --git a/Default (OSX).sublime-keymap b/Default (OSX).sublime-keymap new file mode 100644 index 00000000..0000d03e --- /dev/null +++ b/Default (OSX).sublime-keymap @@ -0,0 +1,5 @@ +[ + { "keys": ["ctrl+super+l"], "command": "sublimelinter", "args": {"action": "lint"} }, + { "keys": ["ctrl+super+e"], "command": "find_next_lint_error" }, + { "keys": ["ctrl+super+shift+e"], "command": "find_previous_lint_error" } +] diff --git a/Default (Windows).sublime-keymap b/Default (Windows).sublime-keymap new file mode 100644 index 00000000..c7c19ede --- /dev/null +++ b/Default (Windows).sublime-keymap @@ -0,0 +1,5 @@ +[ + { "keys": ["ctrl+alt+l"], "command": "sublimelinter", "args": {"action": "lint"} }, + { "keys": ["ctrl+alt+e"], "command": "find_next_lint_error" }, + { "keys": ["ctrl+alt+shift+e"], "command": "find_previous_lint_error" } +] diff --git a/Default.sublime-commands b/Default.sublime-commands new file mode 100644 index 00000000..39c5aa9e --- /dev/null +++ b/Default.sublime-commands @@ -0,0 +1,37 @@ +[ + { + "caption": "SublimeLinter: Lint Current File", + "command": "sublimelinter_lint", + "args": {"action": "lint"} + }, + { + "caption": "SublimeLinter: Show Error List", + "command": "sublimelinter_show_errors", + "args": {"action": "lint", "show_popup": true} + }, + { + "caption": "SublimeLinter: Enable Background Linting", + "command": "sublimelinter_lint", + "args": {"action": "on"} + }, + { + "caption": "SublimeLinter: Enable Load-Save Linting", + "command": "sublimelinter_enable_load_save", + "args": {"action": "load-save"} + }, + { + "caption": "SublimeLinter: Disable Background Linting", + "command": "sublimelinter_disable", + "args": {"action": "off"} + }, + { + "caption": "SublimeLinter: Extract Annotations", + "command": "sublimelinter_annotations", + "args": {} + }, + { + "caption": "SublimeLinter: Reset", + "command": "sublimelinter_lint", + "args": {"action": "reset"} + } +] diff --git a/Main.sublime-menu b/Main.sublime-menu new file mode 100644 index 00000000..1a2f2ee2 --- /dev/null +++ b/Main.sublime-menu @@ -0,0 +1,88 @@ +[ + { + "caption": "Preferences", + "mnemonic": "n", + "id": "preferences", + "children": + [ + { + "caption": "Package Settings", + "mnemonic": "P", + "id": "package-settings", + "children": + [ + { + "caption": "SublimeLinter", + "children": + [ + { + "command": "open_file", + "args": {"file": "${packages}/SublimeLinter/Base File.sublime-settings"}, + "caption": "Settings – Default" + }, + { + "command": "open_file", + "args": {"file": "${packages}/User/Base File.sublime-settings"}, + "caption": "Settings – User" + }, + { + "command": "open_file_settings", + "caption": "Settings – Syntax Specific – User" + }, + { "caption": "-" }, + { + "command": "open_file", + "args": { + "file": "${packages}/SublimeLinter/Default (OSX).sublime-keymap", + "platform": "OSX" + }, + "caption": "Key Bindings – Default" + }, + { + "command": "open_file", + "args": { + "file": "${packages}/SublimeLinter/Default (Linux).sublime-keymap", + "platform": "Linux" + }, + "caption": "Key Bindings – Default" + }, + { + "command": "open_file", + "args": { + "file": "${packages}/SublimeLinter/Default (Windows).sublime-keymap", + "platform": "Windows" + }, + "caption": "Key Bindings – Default" + }, + { + "command": "open_file", + "args": { + "file": "${packages}/User/Default (OSX).sublime-keymap", + "platform": "OSX" + }, + "caption": "Key Bindings – User" + }, + { + "command": "open_file", + "args": { + "file": "${packages}/User/Default (Linux).sublime-keymap", + "platform": "Linux" + }, + "caption": "Key Bindings – User" + }, + { + "command": "open_file", + "args": { + "file": "${packages}/User/Default (Windows).sublime-keymap", + "platform": "Windows" + }, + "caption": "Key Bindings – User" + }, + { "caption": "-" } + ] + } + ] + } + ] + } +] diff --git a/README.markdown b/README.markdown deleted file mode 100644 index 1b44d9c4..00000000 --- a/README.markdown +++ /dev/null @@ -1,31 +0,0 @@ -Sublime Lint -========= - -A code-validating plugin with inline highlighting for the [Sublime Text 2](http://sublimetext.com "Sublime Text 2") editor. - -Supports the following languages: - -* Python - native, moderately-complete lint -* PHP - syntax checking via "php -l" -* Perl - syntax+deprecation checking via "perl -c" -* Ruby - syntax checking via "ruby -wc" - -Installing ------ - -*Without Git:* Download the latest source and copy sublimelint_plugin.py and the sublimelint/ folder to your Sublime Text "User" packages directory. - -*With Git:* Clone the repository in your Sublime Text Packages directory (located one folder above the "User" directory) - -> git clone git://github.com/lunixbochs/sublimelint.git - ----- - -The "User" packages directory is located at: - -* Windows: - %APPDATA%/Sublime Text 2/Packages/User/ -* OS X: - ~/Library/Application Support/Sublime Text 2/Packages/User/ -* Linux: - ~/.Sublime Text 2/Packages/User/ diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..1965e4ac --- /dev/null +++ b/README.rst @@ -0,0 +1,321 @@ +SublimeLinter +============= + +SublimeLinter is a plugin that supports "lint" programs (known as "linters"). SublimeLinter highlights +lines of code the linter deems to contain (potential) errors. It also +supports highlighting special annotations (for example: TODO) so that they +can be quickly located. + +SublimeLinter has built in linters for the following languages: + +* Javascript - lint via built in `jshint `_ +* Objective-J - lint via built-in `capp_lint `_ +* python - native, moderately-complete lint +* ruby - syntax checking via "ruby -wc" +* php - syntax checking via "php -l" +* java - lint via "javac -Xlint" +* perl - syntax+deprecation checking via "perl -c" + +.. image:: http://pledgie.com/campaigns/16512.png?skin_name=chrome + :alt: Click here to lend your support to SublimeLinter and make a donation at pledgie.com! + :target: http://pledgie.com/campaigns/16512 + + +Installing +---------- +**With the Package Control plugin:** The easiest way to install SublimeLinter is through Package Control, which can be found at this site: http://wbond.net/sublime_packages/package_control + +Once you install Package Control, restart ST2 and bring up the Command Palette (``Command+Shift+P`` on OS X, ``Control+Shift+P`` on Linux/Windows). Select "Package Control: Install Package", wait while Package Control fetches the latest package list, then select SublimeLinter when the list appears. The advantage of using this method is that Package Control will automatically keep SublimeLinter up to date with the latest version. + +**Without Git:** Download the latest source from `GitHub `_ and copy the SublimeLinter folder to your Sublime Text "Packages" directory. + +**With Git:** Clone the repository in your Sublime Text "Packages" directory:: + + git clone git://github.com/Kronuz/SublimeLinter.git + + +The "Packages" directory is located at: + +* OS X:: + + ~/Library/Application Support/Sublime Text 2/Packages/ + +* Linux:: + + ~/.Sublime Text 2/Packages/ + +* Windows:: + + %APPDATA%/Sublime Text 2/Packages/ + +Using +----- +SublimeLinter runs in one of three modes, which is determined by the "sublimelinter" user setting: + +* **Background mode (the default)** - When the "sublimelinter" setting is true, linting is performed in the background as you modify a file (if the relevant linter supports it). If you like instant feedback, this is the best way to use SublimeLinter. If you want feedback, but not instantly, you can try another mode or set a minimum queue delay so that the linter will only run after a certain amount of idle time. +* **Load-save mode** - When the "sublimelinter" setting is "load-save", linting is performed only when a file is loaded and after saving. Errors are cleared as soon as the file is modified. +* **On demand mode** - When the "sublimelinter" setting is false, linting is performed only when initiated by you. Use the Control+Command+l (OS X) or Control+Alt+l (Linux/Windows) key equivalent or the Command Palette to lint the current file. If the current file has no associated linter, the command will not be available. + +Within a file whose language/syntax is supported by SublimeLinter, you can control SublimeLinter via the Command Palette (Command+Shift+P on OS X, Control+Shift+P on Linux/Windows). The available commands are: + +* **SublimeLinter: Lint Current File** — Lints the current file, highlights any errors and displays how many errors were found. +* **SublimeLinter: Show Error List** — Lints the current file, highlights any errors and displays a quick panel with any errors that are found. Selecting an item from the quick panel jumps to that line. +* **SublimeLinter: Enable Background Linting** — Enables background linting mode for the current view and lints it. +* **SublimeLinter: Disable Background Linting** — Disables background linting mode for the current view and clears all lint errors. +* **SublimeLinter: Enable Load-Save Linting** — Enables load-save linting mode for the current view and clears all lint errors. +* **SublimeLinter: Reset** — Clears all lint errors and sets the linting mode to the value in the Base File settings. + +Depending on the file and the current state of background enabling, some of the commands will not be available. + +When an error is highlighted by the linter, putting the cursor on the offending line will result in the error message being displayed on the status bar. + +If you want to be shown a popup list of all errors whenever a file is saved, modify the user setting:: + + "sublimelinter_popup_errors_on_save": true + +If there are errors in the file, a quick panel will appear which shows the error message, line number and source code for each error. The starting location of all errors on the line are marked with "^". Selecting an error in the quick panel jumps directly to the location of the first error on that line. + +While editing a file, you can quickly move to the next/previous lint error with the following key equivalents: + +* **OS X**:: + + next: Control+Command+e + prev: Control+Command+Shift+e + +* **Linux, Windows**:: + + next: Control+Alt+e + prev: Control+Alt+Shift+e + +By default the search will wrap. You can turn wrapping off with the user setting:: + + "sublimelinter_wrap_find": false + +Linter-specific notes +~~~~~~~~~~~~~~~~~~~~~ +Following are notes specific to individual linters that you should be aware of: + +* **JavaScript** – This linter runs `jshint `_ using JavaScriptCore on Mac OS X or node.js on other platforms, which can be downloaded from [the node.js site](http://nodejs.org/#download). After installation, if node cannot be found by SublimeLinter, you may have to set the path to node in the "sublimelinter\_executable\_map" setting. See "Configuring" below for info on SublimeLinter settings. + + You may want to modify the options passed to jshint. This can be done globally or on a per-project basis by using the **jshint_options** setting. Refer to the jshint.org site for more information on the configuration options available. + +* **ruby** – If you are using rvm or rbenv, you will probably have to specify the full path to the ruby you are using in the ``sublimelinter_executable_map`` setting. See "Configuring" below for more info. + +* **java** – Because it uses ``javac`` to do linting, each time you run the linter the entire dependency graph of the current file will be checked. Depending on the number of classes you import, this can be **extremely** slow. Also note that you **must** provide the ``-sourcepath``, ``-classpath``, ``-Xlint`` and ``{filename}`` arguments to ``javac`` in your per-project settings. See "Per-project settings" below for more information. + +Configuring +----------- +There are a number of configuration options available to customize the behavior of SublimeLinter and its linters. For the latest information on what options are available, select the menu item ``Preferences->Package Settings->SublimeLinter->Settings - Default``. To change the options in your user settings, select the menu item ``Preferences->File Settings - User``. + +Per-project settings +~~~~~~~~~~~~~~~~~~~~ +SublimeLinter supports per-project/per-language settings. This is useful if a linter requires path configuration on a per-project basis. To edit your project settings, select the menu item ``Project->Edit Project``. If there is no "settings" object at the top level, add one and then add a "SublimeLinter" sub-object, like this:: + + { + "folders": + [ + { + "path": "/Users/aparajita/Projects/foo/src" + } + ], + "settings": + { + "SublimeLinter": + { + } + } + } + +Within the "SublimeLinter" object, you can add a settings object for each language. The language name must match the language item in the linter's CONFIG object, which can be found in the linter's source file in the SublimeLinter/sublimelinter/modules folder. Each language can have two settings: + +* "working_directory" – If present and a valid absolute directory path, the working directory is set to this path before the linter executes. This is useful if you are providing linter arguments that contain paths and you want to use working directory-relative paths instead of absolute paths. +* "lint_args" – If present, it must be a sequence of string arguments to pass to the linter. If your linter expects a filename as an argument, use the argument "{filename}" as a placeholder. Note that if you provide this item, you are responsible for passing **all** required arguments to the linter. + +For example, let's say we are editing a Java project and want to use the "java" linter, which requires a source path and class path. In addition, we want to ignore serialization errors. Our project settings might look like this:: + + { + "folders": + [ + { + "path": "/Users/aparajita/Projects/foo/src" + } + ], + "settings": + { + "SublimeLinter": + { + "java": + { + "working_directory": "/Users/aparajita/Projects/foo", + + "lint_args": + [ + "-sourcepath", "src", + "-classpath", "libs/log4j-1.2.9.jar:libs/commons-logging-1.1.jar", + "-Xlint", "-Xlint:-serial", + "{filename}" + ] + } + } + } + } + + +Customizing colors +~~~~~~~~~~~~~~~~~~ +There are three types of "errors" flagged by sublime lint: illegal, +violation, and warning. For each type, SublimeLinter will indicate the offending +line and the character position at which the error occurred on the line. + +By default SublimeLinter will outline offending lines using the background color +of the "sublimelinter." theme style, and underline the character position +using the background color of the "invalid." theme style, where +is one of the three error types. + +If these styles are not defined, the color will be black when there is a light +background color and black when there is a dark background color. You may +define a single "sublimelinter" or "invalid" style to color all three types, +or define separate substyles for one or more types to color them differently. +Most themes have an "invalid" theme style defined by default. + +If you want to make the offending lines glaringly obvious (perhaps for those +who tend to ignore lint errors), you can set the user setting:: + + "sublimelinter_fill_outlines": true + +When this is set true, lines that have errors will be colored with the background +and foreground color of the "sublime." theme style. Unless you have defined +those styles, this setting should be left false. + +You may also mark lines with errors by putting an "x" in the gutter with the user setting:: + + "sublimelinter_gutter_marks": true + +To customize the colors used for highlighting errors and user notes, add the following +to your theme (adapting the color to your liking):: + + + name + SublimeLinter Annotations + scope + sublimelinter.notes + settings + + background + #FFFFAA + foreground + #FFFFFF + + + + name + SublimeLinter Outline + scope + sublimelinter.illegal + settings + + background + #FF4A52 + foreground + #FFFFFF + + + + name + SublimeLinter Underline + scope + invalid.illegal + settings + + background + #FF0000 + + + + name + SublimeLinter Warning Outline + scope + sublimelinter.warning + settings + + background + #DF9400 + foreground + #FFFFFF + + + + name + SublimeLinter Warning Underline + scope + invalid.warning + settings + + background + #FF0000 + + + + name + SublimeLinter Violation Outline + scope + sublimelinter.violation + settings + + background + #ffffff33 + foreground + #FFFFFF + + + + name + SublimeLinter Violation Underline + scope + invalid.violation + settings + + background + #FF0000 + + + + +Troubleshooting +--------------- +If a linter does not seem to be working, you can check the ST2 console to see if it was enabled. When SublimeLinter is loaded, you will see messages in the console like this:: + + Reloading plugin /Users/aparajita/Library/Application Support/Sublime Text 2/Packages/SublimeLinter/sublimelinter_plugin.py + SublimeLinter: JavaScript loaded + SublimeLinter: annotations loaded + SublimeLinter: Objective-J loaded + SublimeLinter: perl loaded + SublimeLinter: php loaded + SublimeLinter: python loaded + SublimeLinter: ruby loaded + SublimeLinter: pylint loaded + +The first time a linter is asked to lint, it will check to see if it can be enabled. You will then see messages like this:: + + SublimeLinter: JavaScript enabled (using JavaScriptCore) + SublimeLinter: Ruby enabled (using "ruby" for executable) + +Let's say the ruby linter is not working. If you look at the console, you may see a message like this:: + + SublimeLinter: ruby disabled ("ruby" cannot be found) + +This means that the ruby executable cannot be found on your system, which means it is not installed or not in your executable path. + +Creating New Linters +-------------------- +If you wish to create a new linter to support a new language, SublimeLinter makes it easy. Here are the steps involved: + +* Create a new file in sublimelinter/modules. If your linter uses an external executable, you will probably want to copy perl.py. If your linter uses built in code, copy objective-j.py. The convention is to name the file the same as the language that will be linted. + +* Configure the CONFIG dict in your module. See the comments in base\_linter.py for information on the values in that dict. You only need to set the values in your module that differ from the defaults in base\_linter.py, as your module's CONFIG is merged with the default. Note that if your linter uses an external executable that does not take stdin, setting 'input\_method' to INPUT\_METHOD\_TEMP\_FILE will allow interactive linting with that executable. + +* If your linter uses built in code, override ``built_in_check()`` and return the errors found. + +* Override ``parse_errors()`` and process the errors. If your linter overrides ``built_in_check()``, ``parse_errors()`` will receive the result of that method. If your linter uses an external executable, ``parse_errors()`` receives the raw output of the executable, stripped of leading and trailing whitespace. + +If your linter has more complex requirements, see the comments for CONFIG in base\_linter.py, and use the existing linters as guides. diff --git a/SublimeLinter.py b/SublimeLinter.py new file mode 100755 index 00000000..5ecbd918 --- /dev/null +++ b/SublimeLinter.py @@ -0,0 +1,838 @@ +# -*- coding: utf-8 -*- +import os +import sys +import time +import threading + +import sublime +import sublime_plugin + +from sublimelinter.loader import Loader +from sublimelinter.modules.base_linter import INPUT_METHOD_FILE + +LINTERS = {} # mapping of language name to linter module +QUEUE = {} # views waiting to be processed by linter +ERRORS = {} # error messages on given line obtained from linter; they are + # displayed in the status bar when cursor is on line with error +VIOLATIONS = {} # violation messages, they are displayed in the status bar +WARNINGS = {} # warning messages, they are displayed in the status bar +TIMES = {} # collects how long it took the linting to complete +MOD_LOAD = Loader(os.getcwd(), LINTERS) # utility to load (and reload + # if necessary) linter modules [useful when working on plugin] + +# For snappier linting, different delays are used for different linting times: +# (linting time, delays) +DELAYS = ( + (50, (50, 100)), + (100, (100, 300)), + (200, (200, 500)), + (400, (400, 1000)), + (800, (800, 2000)), + (1600, (1600, 3000)), +) + +MARKS = { + 'violation': ('', 'dot'), + 'warning': ('', 'dot'), + 'illegal': ('', 'circle'), +} + + +def get_delay(t, view): + delay = 0 + for _t, d in DELAYS: + if _t <= t: + delay = d + delay = delay or DELAYS[0][1] + + # If the user specifies a delay greater than the built in delay, + # figure they only want to see marks when idle. + minDelay = int(view.settings().get('sublimelinter_delay', 0) * 1000) + if minDelay > delay[1]: + erase_lint_marks(view) + + return (minDelay, minDelay) if minDelay > delay[1] else delay + + +def last_selected_lineno(view): + return view.rowcol(view.sel()[0].end())[0] + + +def update_statusbar(view): + vid = view.id() + lineno = last_selected_lineno(view) + errors = [] + + if vid in ERRORS and lineno in ERRORS[vid]: + errors.extend(ERRORS[vid][lineno]) + + if vid in VIOLATIONS and lineno in VIOLATIONS[vid]: + errors.extend(VIOLATIONS[vid][lineno]) + + if vid in WARNINGS and lineno in WARNINGS[vid]: + errors.extend(WARNINGS[vid][lineno]) + + if errors: + view.set_status('Linter', '; '.join(errors)) + else: + view.erase_status('Linter') + + +def background_run(linter, view, **kwargs): + '''run a linter on a given view if settings is set appropriately''' + if linter: + run_once(linter, view, **kwargs) + + if view.settings().get('sublimelinter_notes'): + highlight_notes(view) + + +def run_once(linter, view, event=None, **kwargs): + '''run a linter on a given view regardless of user setting''' + if linter == LINTERS.get('annotations', None): + highlight_notes(view) + return + + vid = view.id() + ERRORS[vid] = {} + VIOLATIONS[vid] = {} + WARNINGS[vid] = {} + start = time.time() + text = view.substr(sublime.Region(0, view.size())).encode('utf-8') + lines, error_underlines, violation_underlines, warning_underlines, ERRORS[vid], VIOLATIONS[vid], WARNINGS[vid] = linter.run(view, text, view.file_name() or '') + add_lint_marks(view, lines, error_underlines, violation_underlines, warning_underlines) + update_statusbar(view) + end = time.time() + TIMES[vid] = (end - start) * 1000 # Keep how long it took to lint + + if event == 'on_post_save' and view.settings().get('sublimelinter_popup_errors_on_save'): + popup_error_list(view) + + +def popup_error_list(view): + vid = view.id() + errors = ERRORS[vid].copy() + + for message_map in [VIOLATIONS[vid], WARNINGS[vid]]: + for line, messages in message_map.items(): + if line in errors: + errors[line].extend(messages) + else: + errors[line] = messages + + error_regions = get_lint_regions(view) + index = 0 + panel_items = [] + + for line in sorted(errors.keys()): + line_errors = errors[line] + line_text = view.substr(view.full_line(view.text_point(line, 0))) + offset = 0 + + for message in line_errors: + region = error_regions[index] + row, column = view.rowcol(region.begin()) + column += offset + offset += 1 + line_text = u'{0}^{1}'.format(line_text[0:column], line_text[column:]) + index += 1 + + for message in line_errors: + item = [message, u'{0}: {1}'.format(line + 1, line_text.strip())] + panel_items.append(item) + + def on_done(selected_item): + if selected_item == -1: + return + + selected = view.sel() + selected.clear() + + # Traverse backwards to the first error on the line + region_begin = error_regions[selected_item].begin() + row, column = view.rowcol(region_begin) + + while selected_item > 0: + previous_item = selected_item - 1 + begin = error_regions[previous_item].begin() + previous_row, column = view.rowcol(begin) + + if previous_row == row: + selected_item = previous_item + else: + region_begin = error_regions[selected_item].begin() + break + + selected.add(sublime.Region(region_begin, region_begin)) + + # We have to force a move to update the cursor position + view.run_command('move', {'by': 'characters', 'forward': True}) + view.run_command('move', {'by': 'characters', 'forward': False}) + view.show_at_center(region_begin) + + view.window().show_quick_panel(panel_items, on_done) + + +def add_lint_marks(view, lines, error_underlines, violation_underlines, warning_underlines): + '''Adds lint marks to view.''' + vid = view.id() + erase_lint_marks(view) + types = {'warning': warning_underlines, 'violation': violation_underlines, 'illegal': error_underlines} + + for type_name, underlines in types.items(): + if underlines: + view.add_regions('lint-underline-' + type_name, underlines, 'invalid.' + type_name, sublime.DRAW_EMPTY_AS_OVERWRITE) + + if lines: + fill_outlines = view.settings().get('sublimelinter_fill_outlines', False) + gutter_mark_enabled = True if view.settings().get('sublimelinter_gutter_marks', False) else False + + outlines = {'warning': [], 'violation': [], 'illegal': []} + + for line in ERRORS[vid]: + outlines['illegal'].append(view.full_line(view.text_point(line, 0))) + + for line in WARNINGS[vid]: + outlines['warning'].append(view.full_line(view.text_point(line, 0))) + + for line in VIOLATIONS[vid]: + outlines['violation'].append(view.full_line(view.text_point(line, 0))) + + for lint_type in outlines: + if outlines[lint_type]: + args = [ + 'lint-outlines-{0}'.format(lint_type), + outlines[lint_type], + 'sublimelinter.{0}'.format(lint_type), + MARKS[lint_type][gutter_mark_enabled] + ] + if not fill_outlines: + args.append(sublime.DRAW_OUTLINED) + view.add_regions(*args) + + +def erase_lint_marks(view): + '''erase all "lint" error marks from view''' + view.erase_regions('lint-underline-illegal') + view.erase_regions('lint-underline-violation') + view.erase_regions('lint-underline-warning') + view.erase_regions('lint-outlines-illegal') + view.erase_regions('lint-outlines-violation') + view.erase_regions('lint-outlines-warning') + + +def get_lint_regions(view, reverse=False): + # First get all of the underlines, which includes every underlined character + underlines = view.get_regions('lint-underline-illegal') + underlines.extend(view.get_regions('lint-underline-violation')) + underlines.extend(view.get_regions('lint-underline-warning')) + + # Each of these regions is one character, so transform it into the character points + points = sorted([region.begin() for region in underlines]) + + # Now coalesce adjacent characters into a single region + underlines = [] + last_point = -999 + + for point in points: + if point != last_point + 1: + underlines.append(sublime.Region(point, point)) + else: + region = underlines[-1] + underlines[-1] = sublime.Region(region.begin(), point) + + last_point = point + + # Now get all outlines, which includes the entire line where underlines are + outlines = view.get_regions('lint-outlines-illegal') + outlines.extend(view.get_regions('lint-outlines-violation')) + outlines.extend(view.get_regions('lint-outlines-warning')) + + # If an outline region contains an underline region, use only the underline + regions = underlines + + for outline in outlines: + contains_underlines = False + + for underline in underlines: + if outline.contains(underline): + contains_underlines = True + break + + if not contains_underlines: + regions.append(outline) + + return sorted(regions, key=lambda x: x.begin(), reverse=reverse) + + +def select_lint_region(view, region): + selected = view.sel() + selected.clear() + + # Find the first underline region within the region to select. + # If there are none, put the cursor at the beginning of the line. + underlineRegion = find_underline_within(view, region) + + if underlineRegion is None: + underlineRegion = sublime.Region(region.begin(), region.begin()) + + selected.add(underlineRegion) + view.show(underlineRegion, True) + + +def find_underline_within(view, region): + underlines = view.get_regions('lint-underline-illegal') + underlines.extend(view.get_regions('lint-underline-violation')) + underlines.extend(view.get_regions('lint-underline-warning')) + underlines.sort(key=lambda x: x.begin()) + + for underline in underlines: + if region.contains(underline): + return underline + + return None + + +def syntax_name(view): + syntax = os.path.basename(view.settings().get('syntax')) + syntax = os.path.splitext(syntax)[0] + return syntax + + +def select_linter(view, ignore_disabled=False): + '''selects the appropriate linter to use based on language in current view''' + syntax = syntax_name(view) + lc_syntax = syntax.lower() + language = None + linter = None + + if lc_syntax in LINTERS: + language = lc_syntax + else: + syntaxMap = view.settings().get('sublimelinter_syntax_map', {}) + + if syntax in syntaxMap: + language = syntaxMap[syntax] + + if language: + if ignore_disabled: + disabled = [] + else: + disabled = view.settings().get('sublimelinter_disable', []) + + if language not in disabled: + linter = LINTERS[language] + + # If the enabled state is False, it must be checked. + # Enabled checking has to be deferred to first view use because + # user settings cannot be loaded during plugin startup. + if not linter.enabled: + enabled, message = linter.check_enabled(view) + print 'SublimeLinter: {0} {1} ({2})'.format(language, 'enabled' if enabled else 'disabled', message) + + if not enabled: + del LINTERS[language] + linter = None + + return linter + + +def highlight_notes(view): + '''highlight user-specified annotations in a file''' + view.erase_regions('lint-annotations') + text = view.substr(sublime.Region(0, view.size())) + regions = LINTERS['annotations'].built_in_check(view, text, '') + + if regions: + view.add_regions('lint-annotations', regions, 'sublimelinter.annotations', sublime.DRAW_EMPTY_AS_OVERWRITE) + + +def queue_linter(linter, view, timeout, busy_timeout, preemptive=False): + '''Put the current view in a queue to be examined by a linter''' + if linter is None: + erase_lint_marks(view) # may have changed file type and left marks behind + + # No point in queuing anything if no linters will run + if not view.settings().get('sublimelinter_notes'): + return + + # user annotations could be present in all types of files + def _update_view(view): + linter = select_linter(view) + try: + background_run(linter, view) + except RuntimeError, ex: + print ex + + queue(view, _update_view, timeout, busy_timeout, preemptive) + + +def background_linter(): + __lock_.acquire() + try: + views = QUEUE.values() + QUEUE.clear() + finally: + __lock_.release() + + for view, callback, args, kwargs in views: + def _callback(): + callback(view, *args, **kwargs) + sublime.set_timeout(_callback, 0) + +################################################################################ +# Queue dispatcher system: + +queue_dispatcher = background_linter +queue_thread_name = 'background linter' +MAX_DELAY = 10 + + +def queue_loop(): + '''An infinite loop running the linter in a background thread meant to + update the view after user modifies it and then does no further + modifications for some time as to not slow down the UI with linting.''' + global __signaled_, __signaled_first_ + + while __loop_: + #print 'acquire...' + __semaphore_.acquire() + __signaled_first_ = 0 + __signaled_ = 0 + #print 'DISPATCHING!', len(QUEUE) + queue_dispatcher() + + +def queue(view, callback, timeout, busy_timeout=None, preemptive=False, args=[], kwargs={}): + global __signaled_, __signaled_first_ + now = time.time() + __lock_.acquire() + + try: + QUEUE[view.id()] = (view, callback, args, kwargs) + if now < __signaled_ + timeout * 4: + timeout = busy_timeout or timeout + + __signaled_ = now + _delay_queue(timeout, preemptive) + if not __signaled_first_: + __signaled_first_ = __signaled_ + #print 'first', + #print 'queued in', (__signaled_ - now) + finally: + __lock_.release() + + +def _delay_queue(timeout, preemptive): + global __signaled_, __queued_ + now = time.time() + + if not preemptive and now <= __queued_ + 0.01: + return # never delay queues too fast (except preemptively) + + __queued_ = now + _timeout = float(timeout) / 1000 + + if __signaled_first_: + if MAX_DELAY > 0 and now - __signaled_first_ + _timeout > MAX_DELAY: + _timeout -= now - __signaled_first_ + if _timeout < 0: + _timeout = 0 + timeout = int(round(_timeout * 1000, 0)) + + new__signaled_ = now + _timeout - 0.01 + + if __signaled_ >= now - 0.01 and (preemptive or new__signaled_ >= __signaled_ - 0.01): + __signaled_ = new__signaled_ + #print 'delayed to', (preemptive, __signaled_ - now) + + def _signal(): + if time.time() < __signaled_: + return + __semaphore_.release() + sublime.set_timeout(_signal, timeout) + + +def delay_queue(timeout): + __lock_.acquire() + try: + _delay_queue(timeout, False) + finally: + __lock_.release() + + +# only start the thread once - otherwise the plugin will get laggy +# when saving it often. +__semaphore_ = threading.Semaphore(0) +__lock_ = threading.Lock() +__queued_ = 0 +__signaled_ = 0 +__signaled_first_ = 0 + +# First finalize old standing threads: +__loop_ = False +__pre_initialized_ = False + + +def queue_finalize(timeout=None): + global __pre_initialized_ + + for thread in threading.enumerate(): + if thread.isAlive() and thread.name == queue_thread_name: + __pre_initialized_ = True + thread.__semaphore_.release() + thread.join(timeout) +queue_finalize() + +# Initialize background thread: +__loop_ = True +__active_linter_thread = threading.Thread(target=queue_loop, name=queue_thread_name) +__active_linter_thread.__semaphore_ = __semaphore_ +__active_linter_thread.start() + +################################################################################ + +UNRECOGNIZED = ''' +* Unrecognized option * : %s +============================================== + +''' + + +def view_in_tab(view, title, text, file_type): + '''Helper function to display information in a tab. + ''' + tab = view.window().new_file() + tab.set_name(title) + _id = tab.buffer_id() + tab.set_scratch(_id) + tab.settings().set('gutter', True) + tab.settings().set('line_numbers', False) + tab.set_syntax_file(file_type) + ed = tab.begin_edit() + tab.insert(ed, 0, text) + tab.end_edit(ed) + return tab, _id + + +def lint_views(linter): + if not linter: + return + + viewsToLint = [] + + for window in sublime.windows(): + for view in window.views(): + viewLinter = select_linter(view) + + if viewLinter == linter: + viewsToLint.append(view) + + for view in viewsToLint: + queue_linter(linter, view, 0, 0, True) + + +def reload_view_module(view): + for name, linter in LINTERS.items(): + module = sys.modules[linter.__module__] + + if module.__file__ == view.file_name(): + print 'SublimeLinter: reloading language:', linter.language + MOD_LOAD.reload_module(module) + lint_views(linter) + break + + +class LintCommand(sublime_plugin.TextCommand): + '''command to interact with linters''' + + def __init__(self, view): + self.view = view + self.help_called = False + + def run_(self, action): + '''method called by default via view.run_command; + used to dispatch to appropriate method''' + if not action: + return + + try: + lc_action = action.lower() + except AttributeError: + return + + if lc_action == 'reset': + self.reset() + elif lc_action == 'on': + self.on() + elif lc_action == 'load-save': + self.enable_load_save() + elif lc_action == 'off': + self.off() + elif action.lower() in LINTERS: + self._run(lc_action) + + def reset(self): + '''Removes existing lint marks and restores user settings.''' + erase_lint_marks(self.view) + settings = sublime.load_settings('Base File.sublime-settings') + self.view.settings().set('sublimelinter', settings.get('sublimelinter', True)) + + def on(self): + '''Turns background linting on.''' + self.view.settings().set('sublimelinter', True) + queue_linter(select_linter(self.view), self.view, 0, 0, True) + + def enable_load_save(self): + '''Turns load-save linting on.''' + self.view.settings().set('sublimelinter', 'load-save') + erase_lint_marks(self.view) + + def off(self): + '''Turns background linting off.''' + self.view.settings().set('sublimelinter', False) + erase_lint_marks(self.view) + + def _run(self, name): + '''runs an existing linter''' + run_once(LINTERS[name.lower()], self.view) + + +class BackgroundLinter(sublime_plugin.EventListener): + '''This plugin controls a linter meant to work in the background + to provide interactive feedback as a file is edited. It can be + turned off via a setting. + ''' + + def __init__(self): + super(BackgroundLinter, self).__init__() + self.lastSelectedLineNo = -1 + + def on_modified(self, view): + if view.is_scratch(): + return + + if view.settings().get('sublimelinter') != True: + erase_lint_marks(view) + return + + linter = select_linter(view) + + # File-based linters are not invoked during a modify + if linter and linter.input_method == INPUT_METHOD_FILE: + erase_lint_marks(view) + return + + delay = get_delay(TIMES.get(view.id(), 100), view) + queue_linter(linter, view, *delay) + + def on_load(self, view): + if view.is_scratch() or view.settings().get('sublimelinter') == False: + return + background_run(select_linter(view), view, event='on_load') + + def on_post_save(self, view): + if view.is_scratch() or view.settings().get('sublimelinter') == False: + return + + reload_view_module(view) + linter = select_linter(view) + background_run(linter, view, event='on_post_save') + + def on_selection_modified(self, view): + if view.is_scratch(): + return + delay_queue(1000) # on movement, delay queue (to make movement responsive) + + # We only display errors in the status bar for the last line in the current selection. + # If that line number has not changed, there is no point in updating the status bar. + lastSelectedLineNo = last_selected_lineno(view) + + if lastSelectedLineNo != self.lastSelectedLineNo: + self.lastSelectedLineNo = lastSelectedLineNo + update_statusbar(view) + + +class FindLintErrorCommand(sublime_plugin.TextCommand): + '''This command is just a superclass for other commands, it is never enabled.''' + def is_enabled(self): + return select_linter(self.view) is not None + + def find_lint_error(self, forward): + regions = get_lint_regions(self.view, reverse=not forward) + + if len(regions) == 0: + sublime.error_message('No lint errors.') + return + + selected = self.view.sel() + point = selected[0].begin() if forward else selected[-1].end() + regionToSelect = None + + # If going forward, find the first region beginning after the point. + # If going backward, find the first region ending before the point. + # If nothing is found in the given direction, wrap to the first/last region. + if forward: + for index, region in enumerate(regions): + if point < region.begin(): + regionToSelect = region + break + else: + for index, region in enumerate(regions): + if point > region.end(): + regionToSelect = region + break + + # If there is only one error line and the cursor is in that line, we cannot move. + # Otherwise wrap to the first/last error line unless settings disallow that. + if regionToSelect is None and (len(regions) > 1 or not regions[0].contains(point)): + if self.view.settings().get('sublimelinter_wrap_find', True): + regionToSelect = regions[0] + + if regionToSelect is not None: + select_lint_region(self.view, regionToSelect) + else: + sublime.error_message('No {0} lint errors.'.format('next' if forward else 'previous')) + + return regionToSelect + + +class FindNextLintErrorCommand(FindLintErrorCommand): + def run(self, edit): + ''' + Move the cursor to the next lint error in the current view. + The search will wrap to the top unless the sublimelinter_wrap_find + setting is set to false. + ''' + self.find_lint_error(forward=True) + + +class FindPreviousLintErrorCommand(FindLintErrorCommand): + def run(self, edit): + ''' + Move the cursor to the previous lint error in the current view. + The search will wrap to the bottom unless the sublimelinter_wrap_find + setting is set to false. + ''' + self.find_lint_error(forward=False) + + +class SublimelinterWindowCommand(sublime_plugin.WindowCommand): + def is_enabled(self): + view = self.window.active_view() + + if view: + if view.is_scratch(): + return False + else: + return True + else: + return False + + def run_(self, args): + pass + + +class SublimelinterAnnotationsCommand(SublimelinterWindowCommand): + '''Commands to extract annotations and display them in + a file + ''' + def run_(self, args): + linter = LINTERS.get('annotations', None) + + if linter is None: + return + + view = self.window.active_view() + + if not view: + return + + text = view.substr(sublime.Region(0, view.size())) + filename = view.file_name() + notes = linter.extract_annotations(text, view, filename) + _, filename = os.path.split(filename) + annotations_view, _id = view_in_tab(view, 'Annotations from {0}'.format(filename), notes, '') + + +class SublimelinterCommand(SublimelinterWindowCommand): + def is_enabled(self): + enabled = super(SublimelinterCommand, self).is_enabled() + + if not enabled: + return False + + linter = select_linter(self.window.active_view(), ignore_disabled=True) + return linter is not None + + def run_(self, args={}): + view = self.window.active_view() + action = args.get('action', '') + + if view and action: + if action == 'lint': + self.lint_view(view, show_popup_list=args.get('show_popup', False)) + else: + view.run_command('lint', action) + + def lint_view(self, view, show_popup_list): + linter = select_linter(view, ignore_disabled=True) + + if linter: + view.run_command('lint', linter.language) + regions = get_lint_regions(view) + + if regions: + if show_popup_list: + popup_error_list(view) + else: + sublime.error_message('{0} lint error{1}.'.format(len(regions), 's' if len(regions) != 1 else '')) + else: + sublime.error_message('No lint errors.') + else: + syntax = syntax_name(view) + sublime.error_message('No linter for the syntax "{0}"'.format(syntax)) + + +class SublimelinterLintCommand(SublimelinterCommand): + def is_enabled(self): + enabled = super(SublimelinterLintCommand, self).is_enabled() + + if enabled: + view = self.window.active_view() + + if view and view.settings().get('sublimelinter') == True: + return False + + return enabled + + +class SublimelinterShowErrorsCommand(SublimelinterCommand): + def is_enabled(self): + return super(SublimelinterShowErrorsCommand, self).is_enabled() + + +class SublimelinterEnableLoadSaveCommand(SublimelinterCommand): + def is_enabled(self): + enabled = super(SublimelinterEnableLoadSaveCommand, self).is_enabled() + + if enabled: + view = self.window.active_view() + + if view and view.settings().get('sublimelinter') == 'load-save': + return False + + return enabled + + +class SublimelinterDisableCommand(SublimelinterCommand): + def is_enabled(self): + enabled = super(SublimelinterDisableCommand, self).is_enabled() + + if enabled: + view = self.window.active_view() + + if view and not view.settings().get('sublimelinter') == True: + return False + + return enabled diff --git a/package_control.json b/package_control.json new file mode 100644 index 00000000..a5c116b9 --- /dev/null +++ b/package_control.json @@ -0,0 +1,20 @@ +{ + "schema_version": "1.1", + "packages": [ + { + "name": "SublimeLinter", + "description": "Inline lint highlighting for the Sublime Text 2 editor", + "author": "Kronuz", + "homepage": "http://github.com/Kronuz/SublimeLinter", + "last_modified": "2011-12-30 18:10:10", + "platforms": { + "*": [ + { + "version": "1.4.1", + "url": "https://nodeload.github.com/Kronuz/SublimeLinter/zipball/2823e0aef52cb49a03013698b0c8ceec8d80b59e" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/sublimelint/modules/perl.py b/sublimelint/modules/perl.py deleted file mode 100644 index 253b10bc..00000000 --- a/sublimelint/modules/perl.py +++ /dev/null @@ -1,84 +0,0 @@ -# perl.py - sublimelint package for checking perl files - -import subprocess, os -import sublime - -def check(codeString, filename): - info = None - if os.name == 'nt': - info = subprocess.STARTUPINFO() - info.dwFlags |= subprocess.STARTF_USESHOWWINDOW - info.wShowWindow = subprocess.SW_HIDE - - process = subprocess.Popen(('perl', '-c'), - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - startupinfo=info) - result = process.communicate(codeString)[0] - - return result - -# start sublimelint perl plugin -import re -__all__ = ['run', 'language'] -language = 'Perl' - -def run(code, view, filename='untitled'): - errors = check(code, filename) - - lines = set() - underline = [] # leave this here for compatibility with original plugin - - errorMessages = {} - def addMessage(lineno, message): - message = str(message) - if lineno in errorMessages: - errorMessages[lineno].append(message) - else: - errorMessages[lineno] = [message] - - def underlineRange(lineno, position, length=1): - line = view.full_line(view.text_point(lineno, 0)) - position += line.begin() - - for i in xrange(length): - underline.append(sublime.Region(position + i)) - - def underlineRegex(lineno, regex, wordmatch=None, linematch=None): - lines.add(lineno) - offset = 0 - - line = view.full_line(view.text_point(lineno, 0)) - lineText = view.substr(line) - if linematch: - match = re.match(linematch, lineText) - if match: - lineText = match.group('match') - offset = match.start('match') - else: - return - - iters = re.finditer(regex, lineText) - results = [(result.start('underline'), result.end('underline')) for result in iters if - not wordmatch or result.group('underline') == wordmatch] - - for start, end in results: - underlineRange(lineno, start+offset, end-start) - - for line in errors.splitlines(): - match = re.match(r'(?P.+?) at .+? line (?P\d+)(, near "(?P.+?)")?', line) - - if match: - error, line = match.group('error'), match.group('line') - lineno = int(line) - 1 - - near = match.group('near') - if near: - error = '%s, near "%s"' % (error, near) - underlineRegex(lineno, '(?P%s)' % near) - - lines.add(lineno) - addMessage(lineno, error) - - return underline, lines, errorMessages, True diff --git a/sublimelint/modules/php.py b/sublimelint/modules/php.py deleted file mode 100644 index 22734bd4..00000000 --- a/sublimelint/modules/php.py +++ /dev/null @@ -1,46 +0,0 @@ -# php.py - sublimelint package for checking php files - -import subprocess, os - -def check(codeString, filename): - info = None - if os.name == 'nt': - info = subprocess.STARTUPINFO() - info.dwFlags |= subprocess.STARTF_USESHOWWINDOW - info.wShowWindow = subprocess.SW_HIDE - - process = subprocess.Popen(('php', '-l', '-d display_errors=On'), stdin=subprocess.PIPE, stdout=subprocess.PIPE, startupinfo=info) - result = process.communicate(codeString)[0] - - return result - -# start sublimelint php plugin -import re -__all__ = ['run', 'language'] -language = 'PHP' - -def run(code, view, filename='untitled'): - errors = check(code, filename) - - lines = set() - underline = [] # leave this here for compatibility with original plugin - - errorMessages = {} - def addMessage(lineno, message): - message = str(message) - if lineno in errorMessages: - errorMessages[lineno].append(message) - else: - errorMessages[lineno] = [message] - - for line in errors.splitlines(): - match = re.match(r'^Parse error:\s*syntax error,\s*(?P.+?)\s+in\s+.+?\s*line\s+(?P\d+)', line) - - if match: - error, line = match.group('error'), match.group('line') - - lineno = int(line) - 1 - lines.add(lineno) - addMessage(lineno, error) - - return underline, lines, errorMessages, True diff --git a/sublimelint/modules/python.py b/sublimelint/modules/python.py deleted file mode 100644 index cbacc467..00000000 --- a/sublimelint/modules/python.py +++ /dev/null @@ -1,888 +0,0 @@ -# python.py - Lint checking for Python - given filename and contents of the code: -# It provides a list of line numbers to outline and offsets to highlight. -# -# This specific module is a derivative of PyFlakes and part of the SublimeLint project. -# SublimeLint is (c) 2011 Ryan Hileman and licensed under the MIT license. -# URL: http://bochs.info/ -# -# The original copyright notices for this file/project follows: -# -# (c) 2005-2008 Divmod, Inc. -# See LICENSE file for details -# -# The LICENSE file is as follows: -# -# Copyright (c) 2005 Divmod, Inc., http://www.divmod.com/ -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -# todo: -# * fix regex for variable names inside strings (quotes) - -import sublime - -import __builtin__ -import os.path -import compiler -from compiler import ast - -class messages: - class Message(object): - message = '' - message_args = () - def __init__(self, filename, lineno): - self.filename = filename - self.lineno = lineno - def __str__(self): - return self.message % self.message_args - - - class UnusedImport(Message): - message = '%r imported but unused' - def __init__(self, filename, lineno, name): - messages.Message.__init__(self, filename, lineno) - self.name = name - self.message_args = (name,) - - - class RedefinedWhileUnused(Message): - message = 'redefinition of unused %r from line %r' - def __init__(self, filename, lineno, name, orig_lineno): - messages.Message.__init__(self, filename, lineno) - self.name = name - self.orig_lineno = orig_lineno - self.message_args = (name, orig_lineno) - - - class ImportShadowedByLoopVar(Message): - message = 'import %r from line %r shadowed by loop variable' - def __init__(self, filename, lineno, name, orig_lineno): - messages.Message.__init__(self, filename, lineno) - self.name = name - self.orig_lineno = orig_lineno - self.message_args = (name, orig_lineno) - - - class ImportStarUsed(Message): - message = "'from %s import *' used; unable to detect undefined names" - def __init__(self, filename, lineno, modname): - messages.Message.__init__(self, filename, lineno) - self.modname = modname - self.message_args = (modname,) - - - class UndefinedName(Message): - message = 'undefined name %r' - def __init__(self, filename, lineno, name): - messages.Message.__init__(self, filename, lineno) - self.name = name - self.message_args = (name,) - - - - class UndefinedExport(Message): - message = 'undefined name %r in __all__' - def __init__(self, filename, lineno, name): - messages.Message.__init__(self, filename, lineno) - self.name = name - self.message_args = (name,) - - - - class UndefinedLocal(Message): - message = "local variable %r (defined in enclosing scope on line %r) referenced before assignment" - def __init__(self, filename, lineno, name, orig_lineno): - messages.Message.__init__(self, filename, lineno) - self.name = name - self.orig_lineno = orig_lineno - self.message_args = (name, orig_lineno) - - - class DuplicateArgument(Message): - message = 'duplicate argument %r in function definition' - def __init__(self, filename, lineno, name): - messages.Message.__init__(self, filename, lineno) - self.name = name - self.message_args = (name,) - - - class RedefinedFunction(Message): - message = 'redefinition of function %r from line %r' - def __init__(self, filename, lineno, name, orig_lineno): - messages.Message.__init__(self, filename, lineno) - self.name = name - self.orig_lineno = orig_lineno - self.message_args = (name, orig_lineno) - - - class LateFutureImport(Message): - message = 'future import(s) %r after other statements' - def __init__(self, filename, lineno, names): - messages.Message.__init__(self, filename, lineno) - self.names = names - self.message_args = (names,) - - - class UnusedVariable(Message): - """ - Indicates that a variable has been explicity assigned to but not actually - used. - """ - - message = 'local variable %r is assigned to but never used' - def __init__(self, filename, lineno, name): - messages.Message.__init__(self, filename, lineno) - self.name = name - self.message_args = (name,) - -class Binding(object): - """ - Represents the binding of a value to a name. - - The checker uses this to keep track of which names have been bound and - which names have not. See L{Assignment} for a special type of binding that - is checked with stricter rules. - - @ivar used: pair of (L{Scope}, line-number) indicating the scope and - line number that this binding was last used - """ - - def __init__(self, name, source): - self.name = name - self.source = source - self.used = False - - - def __str__(self): - return self.name - - - def __repr__(self): - return '<%s object %r from line %r at 0x%x>' % (self.__class__.__name__, - self.name, - self.source.lineno, - id(self)) - -class UnBinding(Binding): - '''Created by the 'del' operator.''' - - - -class Importation(Binding): - """ - A binding created by an import statement. - - @ivar fullName: The complete name given to the import statement, - possibly including multiple dotted components. - @type fullName: C{str} - """ - def __init__(self, name, source): - self.fullName = name - name = name.split('.')[0] - super(Importation, self).__init__(name, source) - - - -class Argument(Binding): - """ - Represents binding a name as an argument. - """ - - - -class Assignment(Binding): - """ - Represents binding a name with an explicit assignment. - - The checker will raise warnings for any Assignment that isn't used. Also, - the checker does not consider assignments in tuple/list unpacking to be - Assignments, rather it treats them as simple Bindings. - """ - - - -class FunctionDefinition(Binding): - pass - - - -class ExportBinding(Binding): - """ - A binding created by an C{__all__} assignment. If the names in the list - can be determined statically, they will be treated as names for export and - additional checking applied to them. - - The only C{__all__} assignment that can be recognized is one which takes - the value of a literal list containing literal strings. For example:: - - __all__ = ["foo", "bar"] - - Names which are imported and not otherwise used but appear in the value of - C{__all__} will not have an unused import warning reported for them. - """ - def names(self): - """ - Return a list of the names referenced by this binding. - """ - names = [] - if isinstance(self.source, ast.List): - for node in self.source.nodes: - if isinstance(node, ast.Const): - names.append(node.value) - return names - - - -class Scope(dict): - importStarred = False # set to True when import * is found - - - def __repr__(self): - return '<%s at 0x%x %s>' % (self.__class__.__name__, id(self), dict.__repr__(self)) - - - def __init__(self): - super(Scope, self).__init__() - - - -class ClassScope(Scope): - pass - - - -class FunctionScope(Scope): - """ - I represent a name scope for a function. - - @ivar globals: Names declared 'global' in this function. - """ - def __init__(self): - super(FunctionScope, self).__init__() - self.globals = {} - - - -class ModuleScope(Scope): - pass - - -# Globally defined names which are not attributes of the __builtin__ module. -_MAGIC_GLOBALS = ['__file__', '__builtins__'] - - - -class Checker(object): - """ - I check the cleanliness and sanity of Python code. - - @ivar _deferredFunctions: Tracking list used by L{deferFunction}. Elements - of the list are two-tuples. The first element is the callable passed - to L{deferFunction}. The second element is a copy of the scope stack - at the time L{deferFunction} was called. - - @ivar _deferredAssignments: Similar to C{_deferredFunctions}, but for - callables which are deferred assignment checks. - """ - - nodeDepth = 0 - traceTree = False - - def __init__(self, tree, filename='(none)'): - self._deferredFunctions = [] - self._deferredAssignments = [] - self.dead_scopes = [] - self.messages = [] - self.filename = filename - self.scopeStack = [ModuleScope()] - self.futuresAllowed = True - self.handleChildren(tree) - self._runDeferred(self._deferredFunctions) - # Set _deferredFunctions to None so that deferFunction will fail - # noisily if called after we've run through the deferred functions. - self._deferredFunctions = None - self._runDeferred(self._deferredAssignments) - # Set _deferredAssignments to None so that deferAssignment will fail - # noisly if called after we've run through the deferred assignments. - self._deferredAssignments = None - del self.scopeStack[1:] - self.popScope() - self.check_dead_scopes() - - - def deferFunction(self, callable): - ''' - Schedule a function handler to be called just before completion. - - This is used for handling function bodies, which must be deferred - because code later in the file might modify the global scope. When - `callable` is called, the scope at the time this is called will be - restored, however it will contain any new bindings added to it. - ''' - self._deferredFunctions.append((callable, self.scopeStack[:])) - - - def deferAssignment(self, callable): - """ - Schedule an assignment handler to be called just after deferred - function handlers. - """ - self._deferredAssignments.append((callable, self.scopeStack[:])) - - - def _runDeferred(self, deferred): - """ - Run the callables in C{deferred} using their associated scope stack. - """ - for handler, scope in deferred: - self.scopeStack = scope - handler() - - - def scope(self): - return self.scopeStack[-1] - scope = property(scope) - - def popScope(self): - self.dead_scopes.append(self.scopeStack.pop()) - - - def check_dead_scopes(self): - """ - Look at scopes which have been fully examined and report names in them - which were imported but unused. - """ - for scope in self.dead_scopes: - export = isinstance(scope.get('__all__'), ExportBinding) - if export: - all = scope['__all__'].names() - if os.path.split(self.filename)[1] != '__init__.py': - # Look for possible mistakes in the export list - undefined = set(all) - set(scope) - for name in undefined: - self.report( - messages.UndefinedExport, - scope['__all__'].source.lineno, - name) - else: - all = [] - - # Look for imported names that aren't used. - for importation in scope.itervalues(): - if isinstance(importation, Importation): - if not importation.used and importation.name not in all: - self.report( - messages.UnusedImport, - importation.source.lineno, - importation.name) - - - def pushFunctionScope(self): - self.scopeStack.append(FunctionScope()) - - def pushClassScope(self): - self.scopeStack.append(ClassScope()) - - def report(self, messageClass, *args, **kwargs): - self.messages.append(messageClass(self.filename, *args, **kwargs)) - - def handleChildren(self, tree): - for node in tree.getChildNodes(): - self.handleNode(node, tree) - - def handleNode(self, node, parent): - node.parent = parent - if self.traceTree: - print ' ' * self.nodeDepth + node.__class__.__name__ - self.nodeDepth += 1 - nodeType = node.__class__.__name__.upper() - if nodeType not in ('STMT', 'FROM'): - self.futuresAllowed = False - try: - handler = getattr(self, nodeType) - handler(node) - finally: - self.nodeDepth -= 1 - if self.traceTree: - print ' ' * self.nodeDepth + 'end ' + node.__class__.__name__ - - def ignore(self, node): - pass - - STMT = PRINT = PRINTNL = TUPLE = LIST = ASSTUPLE = ASSATTR = \ - ASSLIST = GETATTR = SLICE = SLICEOBJ = IF = CALLFUNC = DISCARD = \ - RETURN = ADD = MOD = SUB = NOT = UNARYSUB = INVERT = ASSERT = COMPARE = \ - SUBSCRIPT = AND = OR = TRYEXCEPT = RAISE = YIELD = DICT = LEFTSHIFT = \ - RIGHTSHIFT = KEYWORD = TRYFINALLY = WHILE = EXEC = MUL = DIV = POWER = \ - FLOORDIV = BITAND = BITOR = BITXOR = LISTCOMPFOR = LISTCOMPIF = \ - AUGASSIGN = BACKQUOTE = UNARYADD = GENEXPR = GENEXPRFOR = GENEXPRIF = \ - IFEXP = handleChildren - - CONST = PASS = CONTINUE = BREAK = ELLIPSIS = ignore - - def addBinding(self, lineno, value, reportRedef=True): - '''Called when a binding is altered. - - - `lineno` is the line of the statement responsible for the change - - `value` is the optional new value, a Binding instance, associated - with the binding; if None, the binding is deleted if it exists. - - if `reportRedef` is True (default), rebinding while unused will be - reported. - ''' - if (isinstance(self.scope.get(value.name), FunctionDefinition) - and isinstance(value, FunctionDefinition)): - self.report(messages.RedefinedFunction, - lineno, value.name, self.scope[value.name].source.lineno) - - if not isinstance(self.scope, ClassScope): - for scope in self.scopeStack[::-1]: - existing = scope.get(value.name) - if (isinstance(existing, Importation) - and not existing.used - and (not isinstance(value, Importation) or value.fullName == existing.fullName) - and reportRedef): - - self.report(messages.RedefinedWhileUnused, - lineno, value.name, scope[value.name].source.lineno) - - if isinstance(value, UnBinding): - try: - del self.scope[value.name] - except KeyError: - self.report(messages.UndefinedName, lineno, value.name) - else: - self.scope[value.name] = value - - - def WITH(self, node): - """ - Handle C{with} by checking the target of the statement (which can be an - identifier, a list or tuple of targets, an attribute, etc) for - undefined names and defining any it adds to the scope and by continuing - to process the suite within the statement. - """ - # Check the "foo" part of a "with foo as bar" statement. Do this no - # matter what, since there's always a "foo" part. - self.handleNode(node.expr, node) - - if node.vars is not None: - self.handleNode(node.vars, node) - - self.handleChildren(node.body) - - - def GLOBAL(self, node): - """ - Keep track of globals declarations. - """ - if isinstance(self.scope, FunctionScope): - self.scope.globals.update(dict.fromkeys(node.names)) - - def LISTCOMP(self, node): - for qual in node.quals: - self.handleNode(qual, node) - self.handleNode(node.expr, node) - - GENEXPRINNER = LISTCOMP - - def FOR(self, node): - """ - Process bindings for loop variables. - """ - vars = [] - def collectLoopVars(n): - if hasattr(n, 'name'): - vars.append(n.name) - else: - for c in n.getChildNodes(): - collectLoopVars(c) - - collectLoopVars(node.assign) - for varn in vars: - if (isinstance(self.scope.get(varn), Importation) - # unused ones will get an unused import warning - and self.scope[varn].used): - self.report(messages.ImportShadowedByLoopVar, - node.lineno, varn, self.scope[varn].source.lineno) - - self.handleChildren(node) - - def NAME(self, node): - """ - Locate the name in locals / function / globals scopes. - """ - # try local scope - importStarred = self.scope.importStarred - try: - self.scope[node.name].used = (self.scope, node.lineno) - except KeyError: - pass - else: - return - - # try enclosing function scopes - - for scope in self.scopeStack[-2:0:-1]: - importStarred = importStarred or scope.importStarred - if not isinstance(scope, FunctionScope): - continue - try: - scope[node.name].used = (self.scope, node.lineno) - except KeyError: - pass - else: - return - - # try global scope - - importStarred = importStarred or self.scopeStack[0].importStarred - try: - self.scopeStack[0][node.name].used = (self.scope, node.lineno) - except KeyError: - if ((not hasattr(__builtin__, node.name)) - and node.name not in _MAGIC_GLOBALS - and not importStarred): - if (os.path.basename(self.filename) == '__init__.py' and - node.name == '__path__'): - # the special name __path__ is valid only in packages - pass - else: - self.report(messages.UndefinedName, node.lineno, node.name) - - - def FUNCTION(self, node): - if getattr(node, "decorators", None) is not None: - self.handleChildren(node.decorators) - self.addBinding(node.lineno, FunctionDefinition(node.name, node)) - self.LAMBDA(node) - - def LAMBDA(self, node): - for default in node.defaults: - self.handleNode(default, node) - - def runFunction(): - args = [] - - def addArgs(arglist): - for arg in arglist: - if isinstance(arg, tuple): - addArgs(arg) - else: - if arg in args: - self.report(messages.DuplicateArgument, node.lineno, arg) - args.append(arg) - - self.pushFunctionScope() - addArgs(node.argnames) - for name in args: - self.addBinding(node.lineno, Argument(name, node), reportRedef=False) - self.handleNode(node.code, node) - def checkUnusedAssignments(): - """ - Check to see if any assignments have not been used. - """ - for name, binding in self.scope.iteritems(): - if (not binding.used and not name in self.scope.globals - and isinstance(binding, Assignment)): - self.report(messages.UnusedVariable, - binding.source.lineno, name) - self.deferAssignment(checkUnusedAssignments) - self.popScope() - - self.deferFunction(runFunction) - - - def CLASS(self, node): - """ - Check names used in a class definition, including its decorators, base - classes, and the body of its definition. Additionally, add its name to - the current scope. - """ - if getattr(node, "decorators", None) is not None: - self.handleChildren(node.decorators) - for baseNode in node.bases: - self.handleNode(baseNode, node) - self.addBinding(node.lineno, Binding(node.name, node)) - self.pushClassScope() - self.handleChildren(node.code) - self.popScope() - - - def ASSNAME(self, node): - if node.flags == 'OP_DELETE': - if isinstance(self.scope, FunctionScope) and node.name in self.scope.globals: - del self.scope.globals[node.name] - else: - self.addBinding(node.lineno, UnBinding(node.name, node)) - else: - # if the name hasn't already been defined in the current scope - if isinstance(self.scope, FunctionScope) and node.name not in self.scope: - # for each function or module scope above us - for scope in self.scopeStack[:-1]: - if not isinstance(scope, (FunctionScope, ModuleScope)): - continue - # if the name was defined in that scope, and the name has - # been accessed already in the current scope, and hasn't - # been declared global - if (node.name in scope - and scope[node.name].used - and scope[node.name].used[0] is self.scope - and node.name not in self.scope.globals): - # then it's probably a mistake - self.report(messages.UndefinedLocal, - scope[node.name].used[1], - node.name, - scope[node.name].source.lineno) - break - - if isinstance(node.parent, - (ast.For, ast.ListCompFor, ast.GenExprFor, - ast.AssTuple, ast.AssList)): - binding = Binding(node.name, node) - elif (node.name == '__all__' and - isinstance(self.scope, ModuleScope) and - isinstance(node.parent, ast.Assign)): - binding = ExportBinding(node.name, node.parent.expr) - else: - binding = Assignment(node.name, node) - if node.name in self.scope: - binding.used = self.scope[node.name].used - self.addBinding(node.lineno, binding) - - def ASSIGN(self, node): - self.handleNode(node.expr, node) - for subnode in node.nodes[::-1]: - self.handleNode(subnode, node) - - def IMPORT(self, node): - for name, alias in node.names: - name = alias or name - importation = Importation(name, node) - self.addBinding(node.lineno, importation) - - def FROM(self, node): - if node.modname == '__future__': - if not self.futuresAllowed: - self.report(messages.LateFutureImport, node.lineno, [n[0] for n in node.names]) - else: - self.futuresAllowed = False - - for name, alias in node.names: - if name == '*': - self.scope.importStarred = True - self.report(messages.ImportStarUsed, node.lineno, node.modname) - continue - name = alias or name - importation = Importation(name, node) - if node.modname == '__future__': - importation.used = (self.scope, node.lineno) - self.addBinding(node.lineno, importation) - -class OffsetError(messages.Message): - message = '%r at offset %r' - def __init__(self, filename, lineno, text, offset): - messages.Message.__init__(self, filename, lineno) - self.offset = offset - self.message_args = (text, offset) - -class PythonError(messages.Message): - message = '%r' - def __init__(self, filename, lineno, text): - messages.Message.__init__(self, filename, lineno) - self.message_args = (text,) - -def check(codeString, filename): - codeString = codeString.rstrip() - try: - try: - compile(codeString, filename, "exec") - except MemoryError: - # Python 2.4 will raise MemoryError if the source can't be - # decoded. - if sys.version_info[:2] == (2, 4): - raise SyntaxError(None) - raise - except (SyntaxError, IndentationError), value: - # print traceback.format_exc() # helps debug new cases - msg = value.args[0] - - lineno, offset, text = value.lineno, value.offset, value.text - - # If there's an encoding problem with the file, the text is None. - if text is None: - # Avoid using msg, since for the only known case, it contains a - # bogus message that claims the encoding the file declared was - # unknown. - if msg.startswith('duplicate argument'): - arg = msg.split('duplicate argument ',1)[1].split(' ',1)[0].strip('\'"') - error = messages.DuplicateArgument(filename, lineno, arg) - else: - error = PythonError(filename, lineno, msg) - else: - line = text.splitlines()[-1] - - if offset is not None: - offset = offset - (len(text) - len(line)) - - if offset is not None: - error = OffsetError(filename, lineno, msg, offset) - else: - error = PythonError(filename, lineno, msg) - - return [error] - except ValueError, e: - return [PythonError(filename, 0, e.args[0])] - else: - # Okay, it's syntactically valid. Now parse it into an ast and check - # it. - tree = compiler.parse(codeString) - w = Checker(tree, filename) - w.messages.sort(lambda a, b: cmp(a.lineno, b.lineno)) - return w.messages - -# end pyflakes -# start sublimelint python plugin - -import sys, re -__all__ = ['run', 'language'] -language = 'Python' - -def run(code, view, filename='untitled'): - stripped_lines = [] - good_lines = [] - lines = code.split('\n') - for i in xrange(len(lines)): - line = lines[i] - if not line.strip() or line.strip().startswith('#'): - stripped_lines.append(i) - else: - good_lines.append(line) - - text = '\n'.join(good_lines) - errors = check(text, filename) - - lines = set() - underline = [] - - def underlineRange(lineno, position, length=1): - line = view.full_line(view.text_point(lineno, 0)) - position += line.begin() - - for i in xrange(length): - underline.append(sublime.Region(position + i)) - - def underlineRegex(lineno, regex, wordmatch=None, linematch=None): - lines.add(lineno) - offset = 0 - - line = view.full_line(view.text_point(lineno, 0)) - lineText = view.substr(line) - if linematch: - match = re.match(linematch, lineText) - if match: - lineText = match.group('match') - offset = match.start('match') - else: - return - - iters = re.finditer(regex, lineText) - results = [(result.start('underline'), result.end('underline')) for result in iters if - not wordmatch or result.group('underline') == wordmatch] - - for start, end in results: - underlineRange(lineno, start+offset, end-start) - - def underlineWord(lineno, word): - regex = r'((and|or|not|if|elif|while|in)\s+|[+\-*^%%<>=\(\{])*\s*(?P[\w\.]*%s[\w]*)' % (word) - underlineRegex(lineno, regex, word) - - def underlineImport(lineno, word): - linematch = '(from\s+[\w_\.]+\s+)?import\s+(?P[^#;]+)' - regex = '(^|\s+|,\s*|as\s+)(?P[\w]*%s[\w]*)' % word - underlineRegex(lineno, regex, word, linematch) - - def underlineForVar(lineno, word): - regex = 'for\s+(?P[\w]*%s[\w*])' % word - underlineRegex(lineno, regex, word) - - def underlineDuplicateArgument(lineno, word): - regex = 'def [\w_]+\(.*?(?P[\w]*%s[\w]*)' % word - underlineRegex(lineno, regex, word) - - errorMessages = {} - def addMessage(lineno, message): - message = str(message) - if lineno in errorMessages: - errorMessages[lineno].append(message) - else: - errorMessages[lineno] = [message] - - for error in errors: - error.lineno -= 1 - for i in stripped_lines: - if error.lineno >= i: - error.lineno += 1 - - lines.add(error.lineno) - addMessage(error.lineno, error) - if isinstance(error, OffsetError): - underlineRange(error.lineno, error.offset) - - elif isinstance(error, PythonError): - pass - - elif isinstance(error, messages.UnusedImport): - underlineImport(error.lineno, error.name) - - elif isinstance(error, messages.RedefinedWhileUnused): - underlineWord(error.lineno, error.name) - - elif isinstance(error, messages.ImportShadowedByLoopVar): - underlineForVar(error.lineno, error.name) - - elif isinstance(error, messages.ImportStarUsed): - underlineImport(error.lineno, '\*') - - elif isinstance(error, messages.UndefinedName): - underlineWord(error.lineno, error.name) - - elif isinstance(error, messages.UndefinedExport): - underlineWord(error.lineno, error.name) - - elif isinstance(error, messages.UndefinedLocal): - underlineWord(error.lineno, error.name) - - elif isinstance(error, messages.DuplicateArgument): - underlineDuplicateArgument(error.lineno, error.name) - - elif isinstance(error, messages.RedefinedFunction): - underlineWord(error.lineno, error.name) - - elif isinstance(error, messages.LateFutureImport): - pass - - elif isinstance(error, messages.UnusedVariable): - underlineWord(error.lineno, error.name) - - else: - print 'Oops, we missed an error type!' - - return underline, lines, errorMessages, True diff --git a/sublimelint/modules/ruby.py b/sublimelint/modules/ruby.py deleted file mode 100644 index d60297b5..00000000 --- a/sublimelint/modules/ruby.py +++ /dev/null @@ -1,55 +0,0 @@ -# ruby.py - sublimelint package for checking ruby files - -import subprocess, os - -def check(codeString, filename): - info = None - if os.name == 'nt': - info = subprocess.STARTUPINFO() - info.dwFlags |= subprocess.STARTF_USESHOWWINDOW - info.wShowWindow = subprocess.SW_HIDE - - process = subprocess.Popen(('ruby', '-wc'), - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - startupinfo=info) - result = process.communicate(codeString)[0] - - return result - -# start sublimelint Ruby plugin -import re -__all__ = ['run', 'language'] -language = 'Ruby' -description =\ -'''* view.run_command("lint", "Ruby") - Turns background linter off and runs the default Ruby linter - (ruby -c, assumed to be on $PATH) on current view. -''' - -def run(code, view, filename='untitled'): - errors = check(code, filename) - - lines = set() - underline = [] # leave this here for compatibility with original plugin - - errorMessages = {} - def addMessage(lineno, message): - message = str(message) - if lineno in errorMessages: - errorMessages[lineno].append(message) - else: - errorMessages[lineno] = [message] - - for line in errors.splitlines(): - match = re.match(r'^.+:(?P\d+):\s+(?P.+)', line) - - if match: - error, line = match.group('error'), match.group('line') - - lineno = int(line) - 1 - lines.add(lineno) - addMessage(lineno, error) - - return underline, lines, errorMessages, True diff --git a/sublimelint_plugin.py b/sublimelint_plugin.py deleted file mode 100755 index e7e32939..00000000 --- a/sublimelint_plugin.py +++ /dev/null @@ -1,188 +0,0 @@ -import sublime, sublime_plugin -import os, sys, glob - -## todo: -# * fix lag (was partially caused by multiple worker threads - evaluate if it's still an issue) - -## language module loading - -# mapping of language name to language module -languages = {} - -# import config -basepath = 'sublimelint/modules' -modpath = basepath.replace('/', '.') -ignore = '__init__', -basedir = os.getcwd() - -def load_module(name): - fullmod = '%s.%s' % (modpath, name) - - # make sure the path didn't change on us (this is needed for submodule reload) - pushd = os.getcwd() - os.chdir(basedir) - - __import__(fullmod) - - # this following line does two things: - # first, we get the actual module from sys.modules, not the base mod returned by __import__ - # second, we get an updated version with reload() so module development is easier - # (save sublimelint_plugin.py to make sublime text reload language submodules) - mod = sys.modules[fullmod] = reload(sys.modules[fullmod]) - - # update module's __file__ to absolute path so we can reload it if saved with sublime text - mod.__file__ = os.path.abspath(mod.__file__).rstrip('co') - - try: - language = mod.language - languages[language] = mod - except AttributeError: - print 'SublimeLint: Error loading %s - no language specified' % modf - except: - print 'SublimeLint: General error importing %s' % modf - - os.chdir(pushd) - -def reload_module(module): - fullmod = module.__name__ - if not fullmod.startswith(modpath): - return - - name = fullmod.replace(modpath+'.', '', 1) - load_module(name) - -for modf in glob.glob('%s/*.py' % basepath): - base, name = os.path.split(modf) - name = name.split('.', 1)[0] - if name in ignore: continue - - load_module(name) - -## bulk of the code - -# TODO: check to see if the types specified after drawType in the codestill work and replace as necessary -drawType = 4 | 32 # from before ST2 had sublime.DRAW_* - -global lineMessages -lineMessages = {} - -def run(module, view): - global lineMessages - vid = view.id() - - text = view.substr(sublime.Region(0, view.size())).encode('utf-8') - - if view.file_name(): - filename = os.path.split(view.file_name())[-1] - else: - filename = 'untitled' - - underline, lines, errorMessages, clearOutlines = module.run(text, view, filename) - lineMessages[vid] = errorMessages - - view.erase_regions('lint-syntax') - view.erase_regions('lint-syntax-underline') - view.erase_regions('lint-underline') - - if clearOutlines: - view.erase_regions('lint-outlines') - - if underline: - view.add_regions('lint-underline', underline, 'keyword', drawType)#sublime.DRAW_EMPTY_AS_OVERWRITE | sublime.DRAW_OUTLINED) - - if lines: - outlines = [view.full_line(view.text_point(lineno, 0)) for lineno in lines] - view.add_regions('lint-outlines', outlines, 'keyword', drawType)#sublime.DRAW_EMPTY_AS_OVERWRITE | sublime.DRAW_OUTLINED) - - -def validate(view): - for language in languages: - if language in view.settings().get("syntax"): - run(languages[language], view) - break - -import time, thread -queue = {} -lookup = {} - -def validate_runner(): # this threaded runner keeps it from slowing down UI while you type - while True: - time.sleep(0.5) - for vid in dict(queue): - if queue[vid] == 0: - v = lookup[vid] - def _view(): - try: - validate(v) - except RuntimeError, excp: - print excp - sublime.set_timeout(_view, 100) - try: del queue[vid] - except: pass - try: del lookup[vid] - except: pass - else: - queue[vid] = 0 - -def validate_hit(view): - for language in languages: - if language in view.settings().get("syntax"): - break - else: - view.erase_regions('lint-syntax') - view.erase_regions('lint-syntax-underline') - view.erase_regions('lint-underline') - view.erase_regions('lint-outlines') - return - - vid = view.id() - lookup[vid] = view - queue[vid] = 1 - -# only start the thread once - otherwise the plugin will get laggy when saving it often -if not 'already' in globals(): - already = True - thread.start_new_thread(validate_runner, ()) - -class pyflakes(sublime_plugin.EventListener): - def __init__(self, *args, **kwargs): - sublime_plugin.EventListener.__init__(self, *args, **kwargs) - self.lastCount = {} - - def on_modified(self, view): - validate_hit(view) - return - - # alternate method which works alright when we don't have threads/set_timeout - # from when I ported to early X beta :P - text = view.substr(sublime.Region(0, view.size())).encode('utf-8') - count = text.count('\n') - if count > 500: return - bid = view.buffer_id() - - if bid in self.lastCount: - if self.lastCount[bid] != count: - validate(view) - - self.lastCount[bid] = count - - def on_load(self, view): - validate(view) - - def on_post_save(self, view): - # this will reload submodules if they are saved with sublime text - for name, module in languages.items(): - if module.__file__ == view.file_name(): - print 'Sublime Lint - Reloading language:', module.language - reload_module(module) - break - - validate_hit(view) - - def on_selection_modified(self, view): - vid = view.id() - lineno = view.rowcol(view.sel()[0].end())[0] - if vid in lineMessages and lineno in lineMessages[vid]: - view.set_status('pyflakes', '; '.join(lineMessages[vid][lineno])) - else: - view.erase_status('pyflakes') diff --git a/sublimelinter/.tempfiles/Foo.class b/sublimelinter/.tempfiles/Foo.class new file mode 100644 index 00000000..1a4b715d Binary files /dev/null and b/sublimelinter/.tempfiles/Foo.class differ diff --git a/sublimelinter/.tempfiles/Gender.class b/sublimelinter/.tempfiles/Gender.class new file mode 100644 index 00000000..b8b5c618 Binary files /dev/null and b/sublimelinter/.tempfiles/Gender.class differ diff --git a/sublimelint/__init__.py b/sublimelinter/__init__.py similarity index 100% rename from sublimelint/__init__.py rename to sublimelinter/__init__.py diff --git a/sublimelinter/loader.py b/sublimelinter/loader.py new file mode 100644 index 00000000..7ec0882c --- /dev/null +++ b/sublimelinter/loader.py @@ -0,0 +1,120 @@ +# Note: Unlike linter modules, changes made to this module will NOT take effect until +# Sublime Text is restarted. + +import glob +import os +import os.path +import sys + +import modules.base_linter as base_linter + +libs_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'modules', 'libs')) + +if libs_path not in sys.path: + sys.path.insert(0, libs_path) + + +class Loader(object): + '''utility class to load (and reload if necessary) SublimeLinter modules''' + def __init__(self, basedir, linters): + '''assign relevant variables and load all existing linter modules''' + self.basedir = basedir + self.basepath = 'sublimelinter/modules' + self.linters = linters + self.modpath = self.basepath.replace('/', '.') + self.ignored = ('__init__', 'base_linter') + self.fix_path() + self.load_all() + + def fix_path(self): + if os.name != 'posix': + return + + path = os.environ['PATH'] + + if path: + dirs = path.split(':') + + if '/usr/local/bin' not in dirs: + dirs.insert(0, '/usr/local/bin') + + if '~/bin' not in dirs and '$HOME/bin' not in dirs: + dirs.append('$HOME/bin') + + os.environ['PATH'] = ':'.join(dirs) + + def load_all(self): + '''loads all existing linter modules''' + for modf in glob.glob('{0}/*.py'.format(self.basepath)): + base, name = os.path.split(modf) + name = name.split('.', 1)[0] + + if name in self.ignored: + continue + + self.load_module(name) + + def load_module(self, name): + '''loads a single linter module''' + fullmod = '{0}.{1}'.format(self.modpath, name) + + # make sure the path didn't change on us (this is needed for submodule reload) + pushd = os.getcwd() + os.chdir(self.basedir) + + __import__(fullmod) + + # this following line of code does two things: + # first, we get the actual module from sys.modules, + # not the base mod returned by __import__ + # second, we get an updated version with reload() + # so module development is easier + # (to make sublime text reload language submodule, + # just save sublimelinter_plugin.py ) + mod = sys.modules[fullmod] = reload(sys.modules[fullmod]) + + # update module's __file__ to absolute path so we can reload it + # if saved with sublime text + mod.__file__ = os.path.abspath(mod.__file__).rstrip('co') + + language = '' + + try: + config = base_linter.CONFIG.copy() + + try: + config.update(mod.CONFIG) + language = config['language'] + except (AttributeError, KeyError): + pass + + if language: + if hasattr(mod, 'Linter'): + linter = mod.Linter(config) + else: + linter = base_linter.BaseLinter(config) + + lc_language = language.lower() + self.linters[lc_language] = linter + print 'SublimeLinter: {0} loaded'.format(language) + else: + print 'SublimeLinter: {0} disabled (no language specified in module)'.format(name) + + except KeyError: + print 'SublimeLinter: general error importing {0} ({1})'.format(name, language or '') + is_enabled = False + + os.chdir(pushd) + + def reload_module(self, module): + '''reload a single linter module + This method is meant to be used when editing a given + linter module so that changes can be viewed immediately + upon saving without having to restart Sublime Text''' + fullmod = module.__name__ + + if not fullmod.startswith(self.modpath): + return + + name = fullmod.replace(self.modpath + '.', '', 1) + self.load_module(name) diff --git a/sublimelint/modules/__init__.py b/sublimelinter/modules/__init__.py similarity index 100% rename from sublimelint/modules/__init__.py rename to sublimelinter/modules/__init__.py diff --git a/sublimelinter/modules/base_linter.py b/sublimelinter/modules/base_linter.py new file mode 100644 index 00000000..629a045b --- /dev/null +++ b/sublimelinter/modules/base_linter.py @@ -0,0 +1,298 @@ +# -*- coding: utf-8 -*- +# base_linter.py - base class for linters + +import os +import os.path +import re +import subprocess + +import sublime + +# If the linter uses an executable that takes stdin, use this input method. +INPUT_METHOD_STDIN = 1 + +# If the linter uses an executable that does not take stdin but you wish to use +# a temp file so that the current view can be linted interactively, use this input method. +# If the current view has been saved, the tempfile will have the same name as the +# view's file, which is necessary for some linters. +INPUT_METHOD_TEMP_FILE = 2 + +# If the linter uses an executable that does not take stdin and you wish to have +# linting occur only on file load and save, use this input method. +INPUT_METHOD_FILE = 3 + +CONFIG = { + # The display language name for this linter. + 'language': '', + + # Linters may either use built in code or use an external executable. This item may have + # one of the following values: + # + # string - An external command (or path to a command) to execute + # None - The linter is considered to be built in + # + # Alternately, your linter class may define the method get_executable(), + # which should return the three-tuple (, , ): + # must be a boolean than indicates whether the executable is available and usable. + # If is True, must be one of: + # - A command string (or path to a command) if an external executable will be used + # - None if built in code will be used + # - False if no suitable executable can be found or the linter should be disabled + # for some other reason. + # is the message that will be shown in the console when the linter is + # loaded, to aid the user in knowing what the status of the linter is. If None or an empty string, + # a default message will be returned based on the value of . Otherwise it + # must be a string. + 'executable': None, + + # If an external executable is being used, this item specifies the arguments + # used when checking the existence of the executable to determine if the linter can be enabled. + # If more than one argument needs to be passed, use a tuple/list. + # Defaults to '-v' if this item is missing. + 'test_existence_args': '-v', + + # If an external executable is being used, this item specifies the arguments to be passed + # when linting. If there is more than one argument, use a tuple/list. + # If the input method is anything other than INPUT_METHOD_STDIN, put a {filename} placeholder in + # the args where the filename should go. + # + # Alternately, if your linter class may define the method get_lint_args(), which should return + # None for no arguments or a tuple/list for one or more arguments. + 'lint_args': None, + + # If an external executable is being used, the method used to pass input to it. Defaults to STDIN. + 'input_method': INPUT_METHOD_STDIN +} + +TEMPFILES_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '.tempfiles')) + +if not os.path.exists(TEMPFILES_DIR): + os.mkdir(TEMPFILES_DIR) + + +class BaseLinter(object): + '''A base class for linters. Your linter module needs to do the following: + + - Set the relevant values in CONFIG + - Override built_in_check() if it uses a built in linter. You may return + whatever value you want, this value will be passed to parse_errors(). + - Override parse_errors() and populate the relevant lists/dicts. The errors + argument passed to parse_errors() is the output of the executable run through strip(). + + If you do subclass and override __init__, be sure to call super(MyLinter, self).__init__(config). + ''' + + def __init__(self, config): + self.language = config['language'] + self.enabled = False + self.executable = config.get('executable', None) + self.test_existence_args = config.get('test_existence_args', ('-v',)) + + if isinstance(self.test_existence_args, basestring): + self.test_existence_args = (self.test_existence_args,) + + self.input_method = config.get('input_method', INPUT_METHOD_STDIN) + self.filename = None + self.lint_args = config.get('lint_args', ()) + + if isinstance(self.lint_args, basestring): + self.lint_args = (self.lint_args,) + + def check_enabled(self, view): + if hasattr(self, 'get_executable'): + try: + self.enabled, self.executable, message = self.get_executable(view) + + if self.enabled and not message: + message = 'using "{0}"'.format(self.executable) if self.executable else 'built in' + except Exception as ex: + self.enabled = False + message = unicode(ex) + else: + self.enabled, message = self._check_enabled(view) + + return (self.enabled, message or '') + + def _check_enabled(self, view): + if self.executable is None: + return (True, 'built in') + elif isinstance(self.executable, basestring): + self.executable = self.get_mapped_executable(view, self.executable) + elif isinstance(self.executable, bool) and self.executable == False: + return (False, 'unknown error') + else: + return (False, 'bad type for CONFIG["executable"]') + + # If we get this far, the executable is external. Test that it can be executed + # and capture stdout and stderr so they don't end up in the system log. + try: + args = [self.executable] + args.extend(self.test_existence_args) + subprocess.Popen(args, startupinfo=self.get_startupinfo(), + stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate() + except OSError: + return (False, '"{0}" cannot be found'.format(self.executable)) + + return (True, 'using "{0}" for executable'.format(self.executable)) + + def _get_lint_args(self, view, code, filename): + if hasattr(self, 'get_lint_args'): + return self.get_lint_args(view, code, filename) or () + else: + lintArgs = self.lint_args or [] + settings = view.settings().get('SublimeLinter', {}).get(self.language, {}) + + if settings: + args = settings.get('lint_args', []) + lintArgs.extend(args) + + cwd = settings.get('working_directory') + + if cwd and os.path.isabs(cwd) and os.path.isdir(cwd): + os.chdir(cwd) + + return [arg.format(filename=filename) for arg in lintArgs] + + def built_in_check(self, view, code, filename): + return '' + + def executable_check(self, view, code, filename): + args = [self.executable] + tempfilePath = None + + if self.input_method == INPUT_METHOD_STDIN: + args.extend(self._get_lint_args(view, code, filename)) + + elif self.input_method == INPUT_METHOD_TEMP_FILE: + if filename: + filename = os.path.basename(filename) + else: + filename = 'view{0}'.format(view.id()) + + tempfilePath = os.path.join(TEMPFILES_DIR, filename) + + with open(tempfilePath, 'w') as f: + f.write(code) + + args.extend(self._get_lint_args(view, code, tempfilePath)) + code = '' + + elif self.input_method == INPUT_METHOD_FILE: + args.extend(self._get_lint_args(view, code, filename)) + code = '' + + else: + return '' + + try: + process = subprocess.Popen(args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + startupinfo=self.get_startupinfo()) + result = process.communicate(code)[0] + finally: + if tempfilePath: + os.remove(tempfilePath) + + return result.strip() + + def parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages): + pass + + def add_message(self, lineno, lines, message, messages): + # Assume lineno is one-based, ST2 wants zero-based line numbers + lineno -= 1 + lines.add(lineno) + message = message[0].upper() + message[1:] + + # Remove trailing period from error message + if message[-1] == '.': + message = message[:-1] + + if lineno in messages: + messages[lineno].append(message) + else: + messages[lineno] = [message] + + def underline_range(self, view, lineno, position, underlines, length=1): + # Assume lineno is one-based, ST2 wants zero-based line numbers + lineno -= 1 + line = view.full_line(view.text_point(lineno, 0)) + position += line.begin() + + for i in xrange(length): + underlines.append(sublime.Region(position + i)) + + def underline_regex(self, view, lineno, regex, lines, underlines, wordmatch=None, linematch=None): + # Assume lineno is one-based, ST2 wants zero-based line numbers + lineno -= 1 + lines.add(lineno) + offset = 0 + line = view.full_line(view.text_point(lineno, 0)) + lineText = view.substr(line) + + if linematch: + match = re.match(linematch, lineText) + + if match: + lineText = match.group('match') + offset = match.start('match') + else: + return + + iters = re.finditer(regex, lineText) + results = [(result.start('underline'), result.end('underline')) for result in iters + if not wordmatch or result.group('underline') == wordmatch] + + # Make the lineno one-based again for underline_range + lineno += 1 + + for start, end in results: + self.underline_range(view, lineno, start + offset, underlines, end - start) + + def run(self, view, code, filename=None): + self.filename = filename + + if self.executable is None: + errors = self.built_in_check(view, code, filename) + else: + errors = self.executable_check(view, code, filename) + + lines = set() + errorUnderlines = [] # leave this here for compatibility with original plugin + errorMessages = {} + violationUnderlines = [] + violationMessages = {} + warningUnderlines = [] + warningMessages = {} + + self.parse_errors(view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages) + return lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages + + def get_mapped_executable(self, view, default): + map = view.settings().get('sublimelinter_executable_map') + + if map: + lang = self.language.lower() + + if lang in map: + return map[lang] + + return default + + def get_startupinfo(self): + info = None + + if os.name == 'nt': + info = subprocess.STARTUPINFO() + info.dwFlags |= subprocess.STARTF_USESHOWWINDOW + info.wShowWindow = subprocess.SW_HIDE + + return info + + def execute_get_output(self, args): + try: + return subprocess.Popen(args, self.get_startupinfo()).communicate()[0] + except: + return '' diff --git a/sublimelinter/modules/coffeescript.py b/sublimelinter/modules/coffeescript.py new file mode 100644 index 00000000..0dceac98 --- /dev/null +++ b/sublimelinter/modules/coffeescript.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# coffeescript.py - sublimelint package for checking coffee files + +import re +import os + +from base_linter import BaseLinter + +CONFIG = { + 'language': 'coffeescript', + 'executable': 'coffee.cmd' if os.name == 'nt' else 'coffee', + 'lint_args': '-l' +} + + +class Linter(BaseLinter): + def parse_errors(self, view, errors, lines, errorUnderlines, + violationUnderlines, warningUnderlines, errorMessages, + violationMessages, warningMessages): + for line in errors.splitlines(): + match = re.match(r'.*?Error: In .+?, Parse error on line ' + r'(?P\d+): (?P.+)', line) + if not match: + match = re.match(r'.*?Error: In .+?, (?P.+) ' + r'on line (?P\d+)', line) + + if match: + line, error = match.group('line'), match.group('error') + self.add_message(int(line), lines, error, errorMessages) diff --git a/sublimelinter/modules/java.py b/sublimelinter/modules/java.py new file mode 100644 index 00000000..00cd8b74 --- /dev/null +++ b/sublimelinter/modules/java.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# java.py - sublimelint package for checking java files + +import os +import os.path +import re + +from base_linter import BaseLinter, INPUT_METHOD_FILE + +CONFIG = { + 'language': 'java', + 'executable': 'javac', + 'test_existence_args': '-version', + 'input_method': INPUT_METHOD_FILE +} + +ERROR_RE = re.compile(r'^(?P.*\.java):(?P\d+): (?Pwarning: )?(?:\[\w+\] )?(?P.*)') +MARK_RE = re.compile(r'^(?P\s*)\^$') + + +class Linter(BaseLinter): + def parse_errors(self, view, errors, lines, errorUnderlines, + violationUnderlines, warningUnderlines, errorMessages, + violationMessages, warningMessages): + it = iter(errors.splitlines()) + + for line in it: + match = re.match(ERROR_RE, line) + + if match: + path = os.path.abspath(match.group('path')) + + if path != self.filename: + continue + + lineNumber = int(match.group('line')) + warning = match.group('warning') + error = match.group('error') + + if warning: + messages = warningMessages + underlines = warningUnderlines + else: + messages = errorMessages + underlines = errorUnderlines + + # Skip forward until we find the marker + position = -1 + + while True: + line = it.next() + match = re.match(MARK_RE, line) + + if match: + position = len(match.group('mark')) + break + + self.add_message(lineNumber, lines, error, messages) + self.underline_range(view, lineNumber, position, underlines) diff --git a/sublimelinter/modules/javascript.py b/sublimelinter/modules/javascript.py new file mode 100644 index 00000000..de63035d --- /dev/null +++ b/sublimelinter/modules/javascript.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# javascript.py - sublimelint package for checking Javascript files + +import os +import json +import subprocess + +from base_linter import BaseLinter + +CONFIG = { + 'language': 'JavaScript' +} + + +class Linter(BaseLinter): + JSC_PATH = '/System/Library/Frameworks/JavaScriptCore.framework/Versions/A/Resources/jsc' + + def __init__(self, config): + super(Linter, self).__init__(config) + self.use_jsc = False + + def get_executable(self, view): + if os.path.exists(self.JSC_PATH): + self.use_jsc = True + return (True, self.JSC_PATH, 'using JavaScriptCore') + try: + path = self.get_mapped_executable(view, 'node') + subprocess.call([path, '-v'], startupinfo=self.get_startupinfo()) + return (True, path, '') + except OSError: + return (False, '', 'JavaScriptCore or node.js is required') + + def get_lint_args(self, view, code, filename): + path = self.jshint_path() + jshint_options = json.dumps(view.settings().get("jshint_options") or {}) + + if self.use_jsc: + args = (os.path.join(path, 'jshint_jsc.js'), '--', str(code.count('\n')), jshint_options, path + os.path.sep) + else: + args = (os.path.join(path, 'jshint_node.js'), jshint_options) + + return args + + def jshint_path(self): + return os.path.join(os.path.dirname(__file__), 'libs', 'jshint') + + def parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages): + errors = json.loads(errors.strip() or '[]') + + for error in errors: + lineno = error['line'] + self.add_message(lineno, lines, error['reason'], errorMessages) + self.underline_range(view, lineno, error['character'] - 1, errorUnderlines) diff --git a/sublimelinter/modules/libs/capp_lint.py b/sublimelinter/modules/libs/capp_lint.py new file mode 100755 index 00000000..eda49234 --- /dev/null +++ b/sublimelinter/modules/libs/capp_lint.py @@ -0,0 +1,1021 @@ +#!/usr/bin/env python +# +# capp_lint.py - Check Objective-J source code formatting, +# according to Cappuccino standards: +# +# http://cappuccino.org/contribute/coding-style.php +# +# Copyright (C) 2011 Aparajita Fishman + +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from __future__ import with_statement +from optparse import OptionParser +from string import Template +import cgi +import cStringIO +import os +import os.path +import re +import sys + +import sublime + + +EXIT_CODE_SHOW_HTML = 205 +EXIT_CODE_SHOW_TOOLTIP = 206 + + +def exit_show_html(html): + sys.stdout.write(html.encode('utf-8')) + sys.exit(EXIT_CODE_SHOW_HTML) + + +def exit_show_tooltip(text): + sys.stdout.write(text) + sys.exit(EXIT_CODE_SHOW_TOOLTIP) + + +def within_textmate(): + return os.getenv('TM_APP_PATH') is not None + + +def tabs2spaces(text, positions=None): + while True: + index = text.find(u'\t') + + if index < 0: + return text + + spaces = u' ' * (4 - (index % 4)) + text = text[0:index] + spaces + text[index + 1:] + + if positions is not None: + positions.append(index) + + +def relative_path(basedir, filename): + if filename.find(basedir) == 0: + filename = filename[len(basedir) + 1:] + + return filename + + +class LintChecker(object): + + VAR_BLOCK_START_RE = re.compile(ur'''(?x) + (?P\s*) # indent before a var keyword + (?Pvar\s+) # var keyword and whitespace after + (?P[a-zA-Z_$]\w*)\s* + (?: + (?P=)\s* + (?P.*) + | + (?P[,;+\-/*%^&|=\\]) + ) + ''') + + SEPARATOR_RE = re.compile(ur'''(?x) + (?P.*) # Everything up to the line separator + (?P[,;+\-/*%^&|=\\]) # The line separator + \s* # Optional whitespace after + $ # End of expression + ''') + + INDENTED_EXPRESSION_RE_TEMPLATE = ur'''(?x) + [ ]{%d} # Placeholder for indent of first identifier that started block + (?P.+) # Expression + ''' + + VAR_BLOCK_RE_TEMPLATE = ur'''(?x) + [ ]{%d} # Placeholder for indent of first identifier that started block + (?P\s*) # Capture any further indent + (?: + (?P[\[\{].*) + | + (?P[a-zA-Z_$]\w*)\s* + (?: + (?P=)\s* + (?P.*) + | + (?P[,;+\-/*%%^&|=\\]) + ) + | + (?P.+) + ) + ''' + + STATEMENT_RE = re.compile(ur'''(?x) + \s*((continue|do|for|function|if|else|return|switch|while|with)\b|\[+\s*[a-zA-Z_$]\w*\s+[a-zA-Z_$]\w*\s*[:\]]) + ''') + + TRAILING_WHITESPACE_RE = re.compile(ur'^.*(\s+)$') + STRIP_LINE_COMMENT_RE = re.compile(ur'(.*)\s*(?://.*|/\*.*\*/\s*)$') + LINE_COMMENT_RE = re.compile(ur'\s*(?:/\*.*\*/\s*|//.*)$') + BLOCK_COMMENT_START_RE = re.compile(ur'\s*/\*.*(?!\*/\s*)$') + BLOCK_COMMENT_END_RE = re.compile(ur'.*?\*/') + METHOD_RE = ur'[-+]\s*\([a-zA-Z_$]\w*\)\s*[a-zA-Z_$]\w*' + FUNCTION_RE = re.compile(ur'\s*function\s*(?P[a-zA-Z_$]\w*)?\(.*\)\s*\{?') + STRING_LITERAL_RE = re.compile(ur'(?]*\)\s*\w+|[a-zA-Z_$]\w*(\+\+|--)|([ -+*/%^&|<>!]=?|&&|\|\||<<|>>>|={1,3}|!==?)\s*[-+][\w(\[])'), 'pass': False}, + + # Replace the contents of literal strings with spaces so we don't get false matches within them + 'preprocess': ( + {'regex': STRIP_LINE_COMMENT_RE, 'replace': ''}, + {'regex': STRING_LITERAL_RE, 'replace': EMPTY_STRING_LITERAL_FUNCTION}, + ), + 'regex': re.compile(ur'(?<=[\w)\]"\']|([ ]))([-+*/%^]|&&?|\|\|?|<<|>>>?)(?=[\w({\["\']|(?(1)\b\b|[ ]))'), + 'error': 'binary operator without surrounding spaces', + 'showPositionForGroup': 2, + 'type': ERROR_TYPE_WARNING + }, + { + # Filter out @import statements, method declarations + 'filter': {'regex': re.compile(ur'^(@import\b|\s*' + METHOD_RE + ')'), 'pass': False}, + + # Replace the contents of literal strings with spaces so we don't get false matches within them + 'preprocess': ( + {'regex': STRIP_LINE_COMMENT_RE, 'replace': ''}, + {'regex': STRING_LITERAL_RE, 'replace': EMPTY_STRING_LITERAL_FUNCTION}, + ), + 'regex': re.compile(ur'(?:[-*/%^&|<>!]=?|&&|\|\||<<|>>>|={1,3}|!==?)\s*(?>>?=)(?=[\w({\["\']|(?(1)\b\b|[ ]))'), + 'error': 'assignment operator without surrounding spaces', + 'showPositionForGroup': 2, + 'type': ERROR_TYPE_WARNING + }, + { + # Filter out @import statements and @implementation/method declarations + 'filter': {'regex': re.compile(ur'^(@import\b|@implementation\b|\s*' + METHOD_RE + ')'), 'pass': False}, + + # Replace the contents of literal strings with spaces so we don't get false matches within them + 'preprocess': ( + {'regex': STRIP_LINE_COMMENT_RE, 'replace': ''}, + {'regex': STRING_LITERAL_RE, 'replace': EMPTY_STRING_LITERAL_FUNCTION}, + ), + 'regex': re.compile(ur'(?<=[\w)\]"\']|([ ]))(===?|!==?|[<>]=?)(?=[\w({\["\']|(?(1)\b\b|[ ]))'), + 'error': 'comparison operator without surrounding spaces', + 'showPositionForGroup': 2, + 'type': ERROR_TYPE_WARNING + }, + { + 'regex': re.compile(ur'^(\s+)' + METHOD_RE + '|^\s*[-+](\()[a-zA-Z_$][\w]*\)\s*[a-zA-Z_$]\w*|^\s*[-+]\s*\([a-zA-Z_$][\w]*\)(\s+)[a-zA-Z_$]\w*'), + 'error': 'extra or missing space in a method declaration', + 'showPositionForGroup': 0, + 'type': ERROR_TYPE_WARNING + }, + { + # Check for brace following a class or method declaration + 'regex': re.compile(ur'^(?:\s*[-+]\s*\([a-zA-Z_$]\w*\)|@implementation)\s*[a-zA-Z_$][\w]*.*?\s*(\{)\s*(?:$|//.*$)'), + 'error': 'braces should be on their own line', + 'showPositionForGroup': 0, + 'type': ERROR_TYPE_ILLEGAL + }, + { + 'regex': re.compile(ur'^\s*var\s+[a-zA-Z_$]\w*\s*=\s*function\s+([a-zA-Z_$]\w*)\s*\('), + 'error': 'function name is ignored', + 'showPositionForGroup': 1, + 'skip': True, + 'type': ERROR_TYPE_WARNING + }, + ) + + VAR_DECLARATIONS = ['none', 'single', 'strict'] + VAR_DECLARATIONS_NONE = 0 + VAR_DECLARATIONS_SINGLE = 1 + VAR_DECLARATIONS_STRICT = 2 + + DIRS_TO_SKIP = ('.git', 'Frameworks', 'Build', 'Resources', 'CommonJS', 'Objective-J') + + ERROR_FORMATS = ('text', 'html') + TEXT_ERROR_SINGLE_FILE_TEMPLATE = Template(u'$lineNum: $message.\n+$line\n') + TEXT_ERROR_MULTI_FILE_TEMPLATE = Template(u'$filename:$lineNum: $message.\n+$line\n') + + def __init__(self, view, basedir='', var_declarations=VAR_DECLARATIONS_SINGLE, verbose=False): + self.view = view + self.basedir = unicode(basedir, 'utf-8') + self.errors = [] + self.errorFiles = [] + self.filesToCheck = [] + self.varDeclarations = var_declarations + self.verbose = verbose + self.sourcefile = None + self.filename = u'' + self.line = u'' + self.lineNum = 0 + self.varIndent = u'' + self.identifierIndent = u'' + + self.fileChecklist = ( + {'title': 'Check variable blocks', 'action': self.check_var_blocks}, + ) + + def run_line_checks(self): + for check in self.LINE_CHECKLIST: + option = check.get('option') + + if option: + default = check.get('optionDefault', False) + + if not self.view.settings().get(option, default): + continue + + line = self.line + lineFilter = check.get('filter') + + if lineFilter: + match = lineFilter['regex'].search(line) + + if (match and not lineFilter['pass']) or (not match and lineFilter['pass']): + continue + + preprocess = check.get('preprocess') + + if preprocess: + if not isinstance(preprocess, (list, tuple)): + preprocess = (preprocess,) + + for processor in preprocess: + regex = processor.get('regex') + + if regex: + line = regex.sub(processor.get('replace', ''), line) + + regex = check.get('regex') + + if not regex: + continue + + match = regex.search(line) + + if not match: + continue + + positions = [] + group = check.get('showPositionForGroup') + + if (check.get('id') == 'tabs'): + line = tabs2spaces(line, positions=positions) + elif group is not None: + line = tabs2spaces(line) + + for match in regex.finditer(line): + if group > 0: + positions.append(match.start(group)) + else: + # group 0 means show the first non-empty match + for i in range(1, len(match.groups()) + 1): + if match.start(i) >= 0: + positions.append(match.start(i)) + break + + self.error(check['error'], line=line, positions=positions, type=check['type']) + + def next_statement(self, expect_line=False, check_line=True): + try: + while True: + raw_line = self.sourcefile.next()[:-1] # strip EOL + + try: + self.line = unicode(raw_line, 'utf-8', 'strict') # convert to Unicode + self.lineNum += 1 + except UnicodeDecodeError: + self.line = unicode(raw_line, 'utf-8', 'replace') + self.lineNum += 1 + self.error('line contains invalid unicode character(s)', type=self.ERROR_TYPE_ILLEGAL) + + if self.verbose: + print u'%d: %s' % (self.lineNum, tabs2spaces(self.line)) + + if check_line: + self.run_line_checks() + + if not self.is_statement(): + continue + + return True + except StopIteration: + if expect_line: + self.error('unexpected EOF', type=self.ERROR_TYPE_ILLEGAL) + raise + + def is_statement(self): + # Skip empty lines + if len(self.line.strip()) == 0: + return False + + # See if we have a line comment, skip that + match = self.LINE_COMMENT_RE.match(self.line) + + if match: + return False + + # Match a block comment start next so we can find its end, + # otherwise we might get false matches on the contents of the block comment. + match = self.BLOCK_COMMENT_START_RE.match(self.line) + + if match: + self.block_comment() + return False + + return True + + def is_expression(self): + match = self.STATEMENT_RE.match(self.line) + return match is None + + def strip_comment(self): + match = self.STRIP_LINE_COMMENT_RE.match(self.expression) + + if match: + self.expression = match.group(1) + + def get_expression(self, lineMatch): + groupdict = lineMatch.groupdict() + + self.expression = groupdict.get('expression') + + if self.expression is None: + self.expression = groupdict.get('bracket') + + if self.expression is None: + self.expression = groupdict.get('indented_expression') + + if self.expression is None: + self.expression = '' + return + + # Remove all quoted strings from the expression so that we don't + # count unmatched pairs inside the strings. + self.expression = self.STRING_LITERAL_RE.sub(self.EMPTY_SELF_STRING_LITERAL_FUNCTION, self.expression) + + self.strip_comment() + self.expression = self.expression.strip() + + def block_comment(self): + 'Find the end of a block comment' + + commentOpenCount = self.line.count('/*') + commentOpenCount -= self.line.count('*/') + + # If there is an open comment block, eat it + if commentOpenCount: + if self.verbose: + print u'%d: BLOCK COMMENT START' % self.lineNum + else: + return + + match = None + + while not match and self.next_statement(expect_line=True, check_line=False): + match = self.BLOCK_COMMENT_END_RE.match(self.line) + + if self.verbose: + print u'%d: BLOCK COMMENT END' % self.lineNum + + def balance_pairs(self, squareOpenCount, curlyOpenCount, parenOpenCount): + # The following lines have to be indented at least as much as the first identifier + # after the var keyword at the start of the block. + if self.verbose: + print "%d: BALANCE BRACKETS: '['=%d, '{'=%d, '('=%d" % (self.lineNum, squareOpenCount, curlyOpenCount, parenOpenCount) + + lineRE = re.compile(self.INDENTED_EXPRESSION_RE_TEMPLATE % len(self.identifierIndent)) + + while True: + # If the expression has open brackets and is terminated, it's an error + match = self.SEPARATOR_RE.match(self.expression) + + if match and match.group('separator') == ';': + unterminated = [] + + if squareOpenCount: + unterminated.append('[') + + if curlyOpenCount: + unterminated.append('{') + + if parenOpenCount: + unterminated.append('(') + + self.error('unbalanced %s' % ' and '.join(unterminated), type=self.ERROR_TYPE_ILLEGAL) + return False + + self.next_statement(expect_line=True) + match = lineRE.match(self.line) + + if not match: + # If it doesn't match, the indent is wrong check the whole line + self.error('incorrect indentation') + self.expression = self.line + self.strip_comment() + else: + # It matches, extract the expression + self.get_expression(match) + + # Update the bracket counts + squareOpenCount += self.expression.count('[') + squareOpenCount -= self.expression.count(']') + curlyOpenCount += self.expression.count('{') + curlyOpenCount -= self.expression.count('}') + parenOpenCount += self.expression.count('(') + parenOpenCount -= self.expression.count(')') + + if squareOpenCount == 0 and curlyOpenCount == 0 and parenOpenCount == 0: + if self.verbose: + print u'%d: BRACKETS BALANCED' % self.lineNum + + # The brackets are closed, this line must be separated + match = self.SEPARATOR_RE.match(self.expression) + + if not match: + self.error('missing statement separator', type=self.ERROR_TYPE_ILLEGAL) + return False + + return True + + def pairs_balanced(self, lineMatchOrBlockMatch): + + groups = lineMatchOrBlockMatch.groupdict() + + if 'assignment' in groups or 'bracket' in groups: + squareOpenCount = self.expression.count('[') + squareOpenCount -= self.expression.count(']') + + curlyOpenCount = self.expression.count('{') + curlyOpenCount -= self.expression.count('}') + + parenOpenCount = self.expression.count('(') + parenOpenCount -= self.expression.count(')') + + if squareOpenCount or curlyOpenCount or parenOpenCount: + # If the brackets were not properly closed or the statement was + # missing a separator, skip the rest of the var block. + if not self.balance_pairs(squareOpenCount, curlyOpenCount, parenOpenCount): + return False + + return True + + def var_block(self, blockMatch): + """ + Parse a var block, return a tuple (haveLine, isSingleVar), where haveLine + indicates whether self.line is the next line to be parsed. + """ + + # Keep track of whether this var block has multiple declarations + isSingleVar = True + + # Keep track of the indent of the var keyword to compare with following lines + self.varIndent = blockMatch.group('indent') + + # Keep track of how far the first variable name is indented to make sure + # following lines line up with that + self.identifierIndent = self.varIndent + blockMatch.group('var') + + # Check the expression to see if we have any open [ or { or /* + self.get_expression(blockMatch) + + if not self.pairs_balanced(blockMatch): + return (False, False) + + separator = '' + + if self.expression: + match = self.SEPARATOR_RE.match(self.expression) + + if not match: + self.error('missing statement separator', type=self.ERROR_TYPE_ILLEGAL) + else: + separator = match.group('separator') + elif blockMatch.group('separator'): + separator = blockMatch.group('separator') + + # If the block has a semicolon, there should be no more lines in the block + blockHasSemicolon = separator == ';' + + # We may not catch an error till after the line that is wrong, so keep + # the most recent declaration and its line number. + lastBlockLine = self.line + lastBlockLineNum = self.lineNum + + # Now construct an RE that will match any lines indented at least as much + # as the var keyword that started the block. + blockRE = re.compile(self.VAR_BLOCK_RE_TEMPLATE % len(self.identifierIndent)) + + while self.next_statement(expect_line=not blockHasSemicolon): + + if not self.is_statement(): + continue + + # Is the line indented at least as much as the var keyword that started the block? + match = blockRE.match(self.line) + + if match: + if self.is_expression(): + lastBlockLine = self.line + lastBlockLineNum = self.lineNum + + # If the line is indented farther than the first identifier in the block, + # it is considered a formatting error. + if match.group('indent') and not match.group('indented_expression'): + self.error('incorrect indentation') + + self.get_expression(match) + + if not self.pairs_balanced(match): + return (False, isSingleVar) + + if self.expression: + separatorMatch = self.SEPARATOR_RE.match(self.expression) + + if separatorMatch is None: + # If the assignment does not have a separator, it's an error + self.error('missing statement separator', type=self.ERROR_TYPE_ILLEGAL) + else: + separator = separatorMatch.group('separator') + + if blockHasSemicolon: + # If the block already has a semicolon, we have an accidental global declaration + self.error('accidental global variable', type=self.ERROR_TYPE_ILLEGAL) + elif (separator == ';'): + blockHasSemicolon = True + elif match.group('separator'): + separator = match.group('separator') + + isSingleVar = False + else: + # If the line is a control statement of some kind, then it should not be indented this far. + self.error('statement should be outdented from preceding var block') + return (True, False) + + else: + # If the line does not match, it is not an assignment or is outdented from the block. + # In either case, the block is considered closed. If the most recent separator was not ';', + # the block was not properly terminated. + if separator != ';': + self.error('unterminated var block', lineNum=lastBlockLineNum, line=lastBlockLine, type=self.ERROR_TYPE_ILLEGAL) + + return (True, isSingleVar) + + def check_var_blocks(self): + lastStatementWasVar = False + lastVarWasSingle = False + haveLine = True + + while True: + if not haveLine: + haveLine = self.next_statement() + + if not self.is_statement(): + haveLine = False + continue + + match = self.VAR_BLOCK_START_RE.match(self.line) + + if match is None: + lastStatementWasVar = False + haveLine = False + continue + + # It might be a function definition, in which case we continue + expression = match.group('expression') + + if expression: + functionMatch = self.FUNCTION_RE.match(expression) + + if functionMatch: + lastStatementWasVar = False + haveLine = False + continue + + # Now we have the start of a variable block + if self.verbose: + print u'%d: VAR BLOCK' % self.lineNum + + varLineNum = self.lineNum + varLine = self.line + + haveLine, isSingleVar = self.var_block(match) + + if self.verbose: + print u'%d: END VAR BLOCK:' % self.lineNum, + + if isSingleVar: + print u'SINGLE' + else: + print u'MULTIPLE' + + if lastStatementWasVar and self.varDeclarations != self.VAR_DECLARATIONS_NONE: + if (self.varDeclarations == self.VAR_DECLARATIONS_SINGLE and lastVarWasSingle and isSingleVar) or \ + (self.varDeclarations == self.VAR_DECLARATIONS_STRICT and (lastVarWasSingle or isSingleVar)): + self.error('consecutive var declarations', lineNum=varLineNum, line=varLine) + + lastStatementWasVar = True + lastVarWasSingle = isSingleVar + + def run_file_checks(self): + for check in self.fileChecklist: + self.sourcefile.seek(0) + self.lineNum = 0 + + if self.verbose: + print u'%s: %s' % (check['title'], self.sourcefile.name) + + check['action']() + + def lint(self, filesToCheck): + # Recursively walk any directories and eliminate duplicates + self.filesToCheck = [] + + for filename in filesToCheck: + filename = unicode(filename, 'utf-8') + fullpath = os.path.join(self.basedir, filename) + + if fullpath not in self.filesToCheck: + if os.path.isdir(fullpath): + for root, dirs, files in os.walk(fullpath): + for skipDir in self.DIRS_TO_SKIP: + if skipDir in dirs: + dirs.remove(skipDir) + + for filename in files: + if not filename.endswith('.j'): + continue + + fullpath = os.path.join(root, filename) + + if fullpath not in self.filesToCheck: + self.filesToCheck.append(fullpath) + else: + self.filesToCheck.append(fullpath) + + for filename in self.filesToCheck: + try: + with open(filename) as self.sourcefile: + self.filename = relative_path(self.basedir, filename) + self.run_file_checks() + + except IOError: + self.lineNum = 0 + self.line = None + self.error('file not found', type=self.ERROR_TYPE_ILLEGAL) + + except StopIteration: + if self.verbose: + print u'EOF\n' + pass + + def lint_text(self, text, filename): + self.filename = filename + self.filesToCheck = [] + + try: + self.sourcefile = cStringIO.StringIO(text) + self.run_file_checks() + except StopIteration: + if self.verbose: + print u'EOF\n' + pass + + def count_files_checked(self): + return len(self.filesToCheck) + + def error(self, message, **kwargs): + info = { + 'filename': self.filename, + 'message': message, + 'type': kwargs.get('type', self.ERROR_TYPE_WARNING) + } + + line = kwargs.get('line', self.line) + lineNum = kwargs.get('lineNum', self.lineNum) + + if line and lineNum: + info['line'] = tabs2spaces(line) + info['lineNum'] = lineNum + + positions = kwargs.get('positions') + + if positions: + info['positions'] = positions + + self.errors.append(info) + + if self.filename not in self.errorFiles: + self.errorFiles.append(self.filename) + + def has_errors(self): + return len(self.errors) != 0 + + def print_errors(self, format='text'): + if not self.errors: + return + + if format == 'text': + self.print_text_errors() + elif format == 'html': + self.print_textmate_html_errors() + elif format == 'tooltip': + self.print_tooltip_errors() + + def print_text_errors(self): + sys.stdout.write('%d error' % len(self.errors)) + + if len(self.errors) > 1: + sys.stdout.write('s') + + if len(self.filesToCheck) == 1: + template = self.TEXT_ERROR_SINGLE_FILE_TEMPLATE + else: + sys.stdout.write(' in %d files' % len(self.errorFiles)) + template = self.TEXT_ERROR_MULTI_FILE_TEMPLATE + + sys.stdout.write(':\n\n') + + for error in self.errors: + if 'lineNum' in error and 'line' in error: + sys.stdout.write(template.substitute(error).encode('utf-8')) + + if error.get('positions'): + markers = ' ' * len(error['line']) + + for position in error['positions']: + markers = markers[:position] + '^' + markers[position + 1:] + + # Add a space at the beginning of the markers to account for the '+' at the beginning + # of the source line. + sys.stdout.write(' %s\n' % markers) + else: + sys.stdout.write('%s: %s.\n' % (error['filename'], error['message'])) + + sys.stdout.write('\n') + + def print_textmate_html_errors(self): + html = """ + + + Cappuccino Lint Report + + + + """ + + html += '

Results: %d error' % len(self.errors) + + if len(self.errors) > 1: + html += 's' + + if len(self.filesToCheck) > 1: + html += ' in %d files' % len(self.errorFiles) + + html += '

' + + for error in self.errors: + message = cgi.escape(error['message']) + + if len(self.filesToCheck) > 1: + filename = cgi.escape(error['filename']) + ':' + else: + filename = '' + + html += '

' + + if 'line' in error and 'lineNum' in error: + filepath = cgi.escape(os.path.join(self.basedir, error['filename'])) + lineNum = error['lineNum'] + line = error['line'] + positions = error.get('positions') + firstPos = -1 + source = '' + + if positions: + firstPos = positions[0] + 1 + lastPos = 0 + + for pos in error.get('positions'): + if pos < len(line): + charToHighlight = line[pos] + else: + charToHighlight = '' + + source += '%s%s' % (cgi.escape(line[lastPos:pos]), cgi.escape(charToHighlight)) + lastPos = pos + 1 + + if lastPos <= len(line): + source += cgi.escape(line[lastPos:]) + else: + source = line + + link = '' % (filepath, lineNum, firstPos) + + if len(self.filesToCheck) > 1: + errorMsg = '%s%d: %s' % (filename, lineNum, message) + else: + errorMsg = '%d: %s' % (lineNum, message) + + html += '%(link)s%(errorMsg)s

\n

%(link)s%(source)s

\n' % {'link': link, 'errorMsg': errorMsg, 'source': source} + else: + html += '%s%s

\n' % (filename, message) + + html += """ + + +""" + exit_show_html(html) + + +if __name__ == '__main__': + usage = 'usage: %prog [options] [file ... | -]' + parser = OptionParser(usage=usage, version='1.02') + parser.add_option('-f', '--format', action='store', type='string', dest='format', default='text', help='the format to use for the report: text (default) or html (HTML in which errors can be clicked on to view in TextMate)') + parser.add_option('-b', '--basedir', action='store', type='string', dest='basedir', help='the base directory relative to which filenames are resolved, defaults to the current working directory') + parser.add_option('-d', '--var-declarations', action='store', type='string', dest='var_declarations', default='single', help='set the policy for flagging consecutive var declarations (%s)' % ', '.join(LintChecker.VAR_DECLARATIONS)) + parser.add_option('-v', '--verbose', action='store_true', dest='verbose', default=False, help='show what lint is doing') + parser.add_option('-q', '--quiet', action='store_true', dest='quiet', default=False, help='do not display errors, only return an exit code') + + (options, args) = parser.parse_args() + + if options.var_declarations not in LintChecker.VAR_DECLARATIONS: + parser.error('--var-declarations must be one of [' + ', '.join(LintChecker.VAR_DECLARATIONS) + ']') + + if options.verbose and options.quiet: + parser.error('options -v/--verbose and -q/--quiet are mutually exclusive') + + options.format = options.format.lower() + + if not options.format in LintChecker.ERROR_FORMATS: + parser.error('format must be one of ' + '/'.join(LintChecker.ERROR_FORMATS)) + + if options.format == 'html' and not within_textmate(): + parser.error('html format can only be used within TextMate.') + + if options.basedir: + basedir = options.basedir + + if basedir[-1] == '/': + basedir = basedir[:-1] + else: + basedir = os.getcwd() + + # We accept a list of filenames (relative to the cwd) either from the command line or from stdin + filenames = args + + if args and args[0] == '-': + filenames = [name.rstrip() for name in sys.stdin.readlines()] + + if not filenames: + print usage.replace('%prog', os.path.basename(sys.argv[0])) + sys.exit(0) + + checker = LintChecker(basedir=basedir, var_declarations=LintChecker.VAR_DECLARATIONS.index(options.var_declarations), verbose=options.verbose) + pathsToCheck = [] + + for filename in filenames: + filename = filename.strip('"\'') + path = os.path.join(basedir, filename) + + if (os.path.isdir(path) and not path.endswith('Frameworks')) or filename.endswith('.j'): + pathsToCheck.append(relative_path(basedir, filename)) + + if len(pathsToCheck) == 0: + if within_textmate(): + exit_show_tooltip('No Objective-J files found.') + + sys.exit(0) + + checker.lint(pathsToCheck) + + if checker.has_errors(): + if not options.quiet: + checker.print_errors(options.format) + + sys.exit(1) + else: + if within_textmate(): + exit_show_tooltip('Everything looks clean.') + + sys.exit(0) diff --git a/sublimelinter/modules/libs/jshint/jshint.js b/sublimelinter/modules/libs/jshint/jshint.js new file mode 100644 index 00000000..464c4c77 --- /dev/null +++ b/sublimelinter/modules/libs/jshint/jshint.js @@ -0,0 +1,4414 @@ +/*! + * JSHint, by JSHint Community. + * + * Licensed under the same slightly modified MIT license that JSLint is. + * It stops evil-doers everywhere. + * + * JSHint is a derivative work of JSLint: + * + * Copyright (c) 2002 Douglas Crockford (www.JSLint.com) + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom + * the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * The Software shall be used for Good, not Evil. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * JSHint was forked from 2010-12-16 edition of JSLint. + * + */ + +/* + JSHINT is a global function. It takes two parameters. + + var myResult = JSHINT(source, option); + + The first parameter is either a string or an array of strings. If it is a + string, it will be split on '\n' or '\r'. If it is an array of strings, it + is assumed that each string represents one line. The source can be a + JavaScript text or a JSON text. + + The second parameter is an optional object of options which control the + operation of JSHINT. Most of the options are booleans: They are all + optional and have a default value of false. One of the options, predef, + can be an array of names, which will be used to declare global variables, + or an object whose keys are used as global names, with a boolean value + that determines if they are assignable. + + If it checks out, JSHINT returns true. Otherwise, it returns false. + + If false, you can inspect JSHINT.errors to find out the problems. + JSHINT.errors is an array of objects containing these members: + + { + line : The line (relative to 0) at which the lint was found + character : The character (relative to 0) at which the lint was found + reason : The problem + evidence : The text line in which the problem occurred + raw : The raw message before the details were inserted + a : The first detail + b : The second detail + c : The third detail + d : The fourth detail + } + + If a fatal error was found, a null will be the last element of the + JSHINT.errors array. + + You can request a Function Report, which shows all of the functions + and the parameters and vars that they use. This can be used to find + implied global variables and other problems. The report is in HTML and + can be inserted in an HTML . + + var myReport = JSHINT.report(limited); + + If limited is true, then the report will be limited to only errors. + + You can request a data structure which contains JSHint's results. + + var myData = JSHINT.data(); + + It returns a structure with this form: + + { + errors: [ + { + line: NUMBER, + character: NUMBER, + reason: STRING, + evidence: STRING + } + ], + functions: [ + name: STRING, + line: NUMBER, + last: NUMBER, + param: [ + STRING + ], + closure: [ + STRING + ], + var: [ + STRING + ], + exception: [ + STRING + ], + outer: [ + STRING + ], + unused: [ + STRING + ], + global: [ + STRING + ], + label: [ + STRING + ] + ], + globals: [ + STRING + ], + member: { + STRING: NUMBER + }, + unused: [ + { + name: STRING, + line: NUMBER + } + ], + implieds: [ + { + name: STRING, + line: NUMBER + } + ], + urls: [ + STRING + ], + json: BOOLEAN + } + + Empty arrays will not be included. + +*/ + +/*jshint + evil: true, nomen: false, onevar: false, regexp: false, strict: true, boss: true, + undef: true, maxlen: 100, indent:4 +*/ + +/*members "\b", "\t", "\n", "\f", "\r", "!=", "!==", "\"", "%", "(begin)", + "(breakage)", "(context)", "(error)", "(global)", "(identifier)", "(last)", + "(line)", "(loopage)", "(name)", "(onevar)", "(params)", "(scope)", + "(statement)", "(verb)", "*", "+", "++", "-", "--", "\/", "<", "<=", "==", + "===", ">", ">=", $, $$, $A, $F, $H, $R, $break, $continue, $w, Abstract, Ajax, + __filename, __dirname, ActiveXObject, Array, ArrayBuffer, ArrayBufferView, Audio, + Autocompleter, Assets, Boolean, Builder, Buffer, Browser, COM, CScript, Canvas, + CustomAnimation, Class, Control, Chain, Color, Cookie, Core, DataView, Date, + Debug, Draggable, Draggables, Droppables, Document, DomReady, DOMReady, DOMParser, Drag, + E, Enumerator, Enumerable, Element, Elements, Error, Effect, EvalError, Event, + Events, FadeAnimation, Field, Flash, Float32Array, Float64Array, Form, + FormField, Frame, FormData, Function, Fx, GetObject, Group, Hash, HotKey, + HTMLElement, HTMLAnchorElement, HTMLBaseElement, HTMLBlockquoteElement, + HTMLBodyElement, HTMLBRElement, HTMLButtonElement, HTMLCanvasElement, HTMLDirectoryElement, + HTMLDivElement, HTMLDListElement, HTMLFieldSetElement, + HTMLFontElement, HTMLFormElement, HTMLFrameElement, HTMLFrameSetElement, + HTMLHeadElement, HTMLHeadingElement, HTMLHRElement, HTMLHtmlElement, + HTMLIFrameElement, HTMLImageElement, HTMLInputElement, HTMLIsIndexElement, + HTMLLabelElement, HTMLLayerElement, HTMLLegendElement, HTMLLIElement, + HTMLLinkElement, HTMLMapElement, HTMLMenuElement, HTMLMetaElement, + HTMLModElement, HTMLObjectElement, HTMLOListElement, HTMLOptGroupElement, + HTMLOptionElement, HTMLParagraphElement, HTMLParamElement, HTMLPreElement, + HTMLQuoteElement, HTMLScriptElement, HTMLSelectElement, HTMLStyleElement, + HtmlTable, HTMLTableCaptionElement, HTMLTableCellElement, HTMLTableColElement, + HTMLTableElement, HTMLTableRowElement, HTMLTableSectionElement, + HTMLTextAreaElement, HTMLTitleElement, HTMLUListElement, HTMLVideoElement, + Iframe, IframeShim, Image, Int16Array, Int32Array, Int8Array, + Insertion, InputValidator, JSON, Keyboard, Locale, LN10, LN2, LOG10E, LOG2E, + MAX_VALUE, MIN_VALUE, Mask, Math, MenuItem, MessageChannel, MessageEvent, MessagePort, + MoveAnimation, MooTools, Native, NEGATIVE_INFINITY, Number, Object, ObjectRange, Option, + Options, OverText, PI, POSITIVE_INFINITY, PeriodicalExecuter, Point, Position, Prototype, + RangeError, Rectangle, ReferenceError, RegExp, ResizeAnimation, Request, RotateAnimation, + SQRT1_2, SQRT2, ScrollBar, ScriptEngine, ScriptEngineBuildVersion, + ScriptEngineMajorVersion, ScriptEngineMinorVersion, Scriptaculous, Scroller, + Slick, Slider, Selector, SharedWorker, String, Style, SyntaxError, Sortable, Sortables, + SortableObserver, Sound, Spinner, System, Swiff, Text, TextArea, Template, + Timer, Tips, Type, TypeError, Toggle, Try, "use strict", unescape, URI, URIError, URL, + VBArray, WSH, WScript, XDomainRequest, Web, Window, XMLDOM, XMLHttpRequest, XMLSerializer, + XPathEvaluator, XPathException, XPathExpression, XPathNamespace, XPathNSResolver, XPathResult, + "\\", a, addEventListener, address, alert, apply, applicationCache, arguments, arity, asi, atob, + b, basic, basicToken, bitwise, block, blur, boolOptions, boss, browser, btoa, c, call, callee, + caller, cases, charAt, charCodeAt, character, clearInterval, clearTimeout, + close, closed, closure, comment, condition, confirm, console, constructor, + content, couch, create, css, curly, d, data, datalist, dd, debug, decodeURI, + decodeURIComponent, defaultStatus, defineClass, deserialize, devel, document, + dojo, dijit, dojox, define, else, emit, encodeURI, encodeURIComponent, + entityify, eqeqeq, eqnull, errors, es5, escape, esnext, eval, event, evidence, evil, + ex, exception, exec, exps, expr, exports, FileReader, first, floor, focus, + forin, fragment, frames, from, fromCharCode, fud, funcscope, funct, function, functions, + g, gc, getComputedStyle, getRow, getter, getterToken, GLOBAL, global, globals, globalstrict, + hasOwnProperty, help, history, i, id, identifier, immed, implieds, importPackage, include, + indent, indexOf, init, ins, instanceOf, isAlpha, isApplicationRunning, isArray, + isDigit, isFinite, isNaN, iterator, java, join, jshint, + JSHINT, json, jquery, jQuery, keys, label, labelled, last, lastsemic, laxbreak, laxcomma, + latedef, lbp, led, left, length, line, load, loadClass, localStorage, location, + log, loopfunc, m, match, maxerr, maxlen, member,message, meta, module, moveBy, + moveTo, mootools, multistr, name, navigator, new, newcap, noarg, node, noempty, nomen, + nonew, nonstandard, nud, onbeforeunload, onblur, onerror, onevar, onecase, onfocus, + onload, onresize, onunload, open, openDatabase, openURL, opener, opera, options, outer, param, + parent, parseFloat, parseInt, passfail, plusplus, predef, print, process, prompt, + proto, prototype, prototypejs, provides, push, quit, range, raw, reach, reason, regexp, + readFile, readUrl, regexdash, removeEventListener, replace, report, require, + reserved, resizeBy, resizeTo, resolvePath, resumeUpdates, respond, rhino, right, + runCommand, scroll, screen, scripturl, scrollBy, scrollTo, scrollbar, search, seal, + send, serialize, sessionStorage, setInterval, setTimeout, setter, setterToken, shift, slice, + smarttabs, sort, spawn, split, stack, status, start, strict, sub, substr, supernew, shadow, + supplant, sum, sync, test, toLowerCase, toString, toUpperCase, toint32, token, top, trailing, + type, typeOf, Uint16Array, Uint32Array, Uint8Array, undef, undefs, unused, urls, validthis, + value, valueOf, var, version, WebSocket, white, window, Worker, wsh*/ + +/*global exports: false */ + +// We build the application inside a function so that we produce only a single +// global variable. That function will be invoked immediately, and its return +// value is the JSHINT function itself. + +var JSHINT = (function () { + "use strict"; + + var anonname, // The guessed name for anonymous functions. + +// These are operators that should not be used with the ! operator. + + bang = { + '<' : true, + '<=' : true, + '==' : true, + '===': true, + '!==': true, + '!=' : true, + '>' : true, + '>=' : true, + '+' : true, + '-' : true, + '*' : true, + '/' : true, + '%' : true + }, + + // These are the JSHint boolean options. + boolOptions = { + asi : true, // if automatic semicolon insertion should be tolerated + bitwise : true, // if bitwise operators should not be allowed + boss : true, // if advanced usage of assignments should be allowed + browser : true, // if the standard browser globals should be predefined + couch : true, // if CouchDB globals should be predefined + curly : true, // if curly braces around all blocks should be required + debug : true, // if debugger statements should be allowed + devel : true, // if logging globals should be predefined (console, + // alert, etc.) + dojo : true, // if Dojo Toolkit globals should be predefined + eqeqeq : true, // if === should be required + eqnull : true, // if == null comparisons should be tolerated + es5 : true, // if ES5 syntax should be allowed + esnext : true, // if es.next specific syntax should be allowed + evil : true, // if eval should be allowed + expr : true, // if ExpressionStatement should be allowed as Programs + forin : true, // if for in statements must filter + funcscope : true, // if only function scope should be used for scope tests + globalstrict: true, // if global "use strict"; should be allowed (also + // enables 'strict') + immed : true, // if immediate invocations must be wrapped in parens + iterator : true, // if the `__iterator__` property should be allowed + jquery : true, // if jQuery globals should be predefined + lastsemic : true, // if semicolons may be ommitted for the trailing + // statements inside of a one-line blocks. + latedef : true, // if the use before definition should not be tolerated + laxbreak : true, // if line breaks should not be checked + laxcomma : true, // if line breaks should not be checked around commas + loopfunc : true, // if functions should be allowed to be defined within + // loops + mootools : true, // if MooTools globals should be predefined + multistr : true, // allow multiline strings + newcap : true, // if constructor names must be capitalized + noarg : true, // if arguments.caller and arguments.callee should be + // disallowed + node : true, // if the Node.js environment globals should be + // predefined + noempty : true, // if empty blocks should be disallowed + nonew : true, // if using `new` for side-effects should be disallowed + nonstandard : true, // if non-standard (but widely adopted) globals should + // be predefined + nomen : true, // if names should be checked + onevar : true, // if only one var statement per function should be + // allowed + onecase : true, // if one case switch statements should be allowed + passfail : true, // if the scan should stop on first error + plusplus : true, // if increment/decrement should not be allowed + proto : true, // if the `__proto__` property should be allowed + prototypejs : true, // if Prototype and Scriptaculous globals should be + // predefined + regexdash : true, // if unescaped first/last dash (-) inside brackets + // should be tolerated + regexp : true, // if the . should not be allowed in regexp literals + rhino : true, // if the Rhino environment globals should be predefined + undef : true, // if variables should be declared before used + scripturl : true, // if script-targeted URLs should be tolerated + shadow : true, // if variable shadowing should be tolerated + smarttabs : true, // if smarttabs should be tolerated + // (http://www.emacswiki.org/emacs/SmartTabs) + strict : true, // require the "use strict"; pragma + sub : true, // if all forms of subscript notation are tolerated + supernew : true, // if `new function () { ... };` and `new Object;` + // should be tolerated + trailing : true, // if trailing whitespace rules apply + validthis : true, // if 'this' inside a non-constructor function is valid. + // This is a function scoped option only. + white : true, // if strict whitespace rules apply + wsh : true // if the Windows Scripting Host environment globals + // should be predefined + }, + + // browser contains a set of global names which are commonly provided by a + // web browser environment. + browser = { + ArrayBuffer : false, + ArrayBufferView : false, + Audio : false, + addEventListener : false, + applicationCache : false, + atob : false, + blur : false, + btoa : false, + clearInterval : false, + clearTimeout : false, + close : false, + closed : false, + DataView : false, + DOMParser : false, + defaultStatus : false, + document : false, + event : false, + FileReader : false, + Float32Array : false, + Float64Array : false, + FormData : false, + focus : false, + frames : false, + getComputedStyle : false, + HTMLElement : false, + HTMLAnchorElement : false, + HTMLBaseElement : false, + HTMLBlockquoteElement : false, + HTMLBodyElement : false, + HTMLBRElement : false, + HTMLButtonElement : false, + HTMLCanvasElement : false, + HTMLDirectoryElement : false, + HTMLDivElement : false, + HTMLDListElement : false, + HTMLFieldSetElement : false, + HTMLFontElement : false, + HTMLFormElement : false, + HTMLFrameElement : false, + HTMLFrameSetElement : false, + HTMLHeadElement : false, + HTMLHeadingElement : false, + HTMLHRElement : false, + HTMLHtmlElement : false, + HTMLIFrameElement : false, + HTMLImageElement : false, + HTMLInputElement : false, + HTMLIsIndexElement : false, + HTMLLabelElement : false, + HTMLLayerElement : false, + HTMLLegendElement : false, + HTMLLIElement : false, + HTMLLinkElement : false, + HTMLMapElement : false, + HTMLMenuElement : false, + HTMLMetaElement : false, + HTMLModElement : false, + HTMLObjectElement : false, + HTMLOListElement : false, + HTMLOptGroupElement : false, + HTMLOptionElement : false, + HTMLParagraphElement : false, + HTMLParamElement : false, + HTMLPreElement : false, + HTMLQuoteElement : false, + HTMLScriptElement : false, + HTMLSelectElement : false, + HTMLStyleElement : false, + HTMLTableCaptionElement : false, + HTMLTableCellElement : false, + HTMLTableColElement : false, + HTMLTableElement : false, + HTMLTableRowElement : false, + HTMLTableSectionElement : false, + HTMLTextAreaElement : false, + HTMLTitleElement : false, + HTMLUListElement : false, + HTMLVideoElement : false, + history : false, + Int16Array : false, + Int32Array : false, + Int8Array : false, + Image : false, + length : false, + localStorage : false, + location : false, + MessageChannel : false, + MessageEvent : false, + MessagePort : false, + moveBy : false, + moveTo : false, + name : false, + navigator : false, + onbeforeunload : true, + onblur : true, + onerror : true, + onfocus : true, + onload : true, + onresize : true, + onunload : true, + open : false, + openDatabase : false, + opener : false, + Option : false, + parent : false, + print : false, + removeEventListener : false, + resizeBy : false, + resizeTo : false, + screen : false, + scroll : false, + scrollBy : false, + scrollTo : false, + sessionStorage : false, + setInterval : false, + setTimeout : false, + SharedWorker : false, + status : false, + top : false, + Uint16Array : false, + Uint32Array : false, + Uint8Array : false, + WebSocket : false, + window : false, + Worker : false, + XMLHttpRequest : false, + XMLSerializer : false, + XPathEvaluator : false, + XPathException : false, + XPathExpression : false, + XPathNamespace : false, + XPathNSResolver : false, + XPathResult : false + }, + + couch = { + "require" : false, + respond : false, + getRow : false, + emit : false, + send : false, + start : false, + sum : false, + log : false, + exports : false, + module : false, + provides : false + }, + + devel = { + alert : false, + confirm : false, + console : false, + Debug : false, + opera : false, + prompt : false + }, + + dojo = { + dojo : false, + dijit : false, + dojox : false, + define : false, + "require" : false + }, + + escapes = { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '/' : '\\/', + '\\': '\\\\' + }, + + funct, // The current function + + functionicity = [ + 'closure', 'exception', 'global', 'label', + 'outer', 'unused', 'var' + ], + + functions, // All of the functions + + global, // The global scope + implied, // Implied globals + inblock, + indent, + jsonmode, + + jquery = { + '$' : false, + jQuery : false + }, + + lines, + lookahead, + member, + membersOnly, + + mootools = { + '$' : false, + '$$' : false, + Assets : false, + Browser : false, + Chain : false, + Class : false, + Color : false, + Cookie : false, + Core : false, + Document : false, + DomReady : false, + DOMReady : false, + Drag : false, + Element : false, + Elements : false, + Event : false, + Events : false, + Fx : false, + Group : false, + Hash : false, + HtmlTable : false, + Iframe : false, + IframeShim : false, + InputValidator : false, + instanceOf : false, + Keyboard : false, + Locale : false, + Mask : false, + MooTools : false, + Native : false, + Options : false, + OverText : false, + Request : false, + Scroller : false, + Slick : false, + Slider : false, + Sortables : false, + Spinner : false, + Swiff : false, + Tips : false, + Type : false, + typeOf : false, + URI : false, + Window : false + }, + + nexttoken, + + node = { + __filename : false, + __dirname : false, + Buffer : false, + console : false, + exports : false, + GLOBAL : false, + global : false, + module : false, + process : false, + require : false, + setTimeout : false, + clearTimeout : false, + setInterval : false, + clearInterval : false + }, + + noreach, + option, + predefined, // Global variables defined by option + prereg, + prevtoken, + + prototypejs = { + '$' : false, + '$$' : false, + '$A' : false, + '$F' : false, + '$H' : false, + '$R' : false, + '$break' : false, + '$continue' : false, + '$w' : false, + Abstract : false, + Ajax : false, + Class : false, + Enumerable : false, + Element : false, + Event : false, + Field : false, + Form : false, + Hash : false, + Insertion : false, + ObjectRange : false, + PeriodicalExecuter: false, + Position : false, + Prototype : false, + Selector : false, + Template : false, + Toggle : false, + Try : false, + Autocompleter : false, + Builder : false, + Control : false, + Draggable : false, + Draggables : false, + Droppables : false, + Effect : false, + Sortable : false, + SortableObserver : false, + Sound : false, + Scriptaculous : false + }, + + rhino = { + defineClass : false, + deserialize : false, + gc : false, + help : false, + importPackage: false, + "java" : false, + load : false, + loadClass : false, + print : false, + quit : false, + readFile : false, + readUrl : false, + runCommand : false, + seal : false, + serialize : false, + spawn : false, + sync : false, + toint32 : false, + version : false + }, + + scope, // The current scope + stack, + + // standard contains the global names that are provided by the + // ECMAScript standard. + standard = { + Array : false, + Boolean : false, + Date : false, + decodeURI : false, + decodeURIComponent : false, + encodeURI : false, + encodeURIComponent : false, + Error : false, + 'eval' : false, + EvalError : false, + Function : false, + hasOwnProperty : false, + isFinite : false, + isNaN : false, + JSON : false, + Math : false, + Number : false, + Object : false, + parseInt : false, + parseFloat : false, + RangeError : false, + ReferenceError : false, + RegExp : false, + String : false, + SyntaxError : false, + TypeError : false, + URIError : false + }, + + // widely adopted global names that are not part of ECMAScript standard + nonstandard = { + escape : false, + unescape : false + }, + + standard_member = { + E : true, + LN2 : true, + LN10 : true, + LOG2E : true, + LOG10E : true, + MAX_VALUE : true, + MIN_VALUE : true, + NEGATIVE_INFINITY : true, + PI : true, + POSITIVE_INFINITY : true, + SQRT1_2 : true, + SQRT2 : true + }, + + directive, + syntax = {}, + tab, + token, + urls, + useESNextSyntax, + warnings, + + wsh = { + ActiveXObject : true, + Enumerator : true, + GetObject : true, + ScriptEngine : true, + ScriptEngineBuildVersion : true, + ScriptEngineMajorVersion : true, + ScriptEngineMinorVersion : true, + VBArray : true, + WSH : true, + WScript : true, + XDomainRequest : true + }; + + // Regular expressions. Some of these are stupidly long. + var ax, cx, tx, nx, nxg, lx, ix, jx, ft; + (function () { + /*jshint maxlen:300 */ + + // unsafe comment or string + ax = /@cc|<\/?|script|\]\s*\]|<\s*!|</i; + + // unsafe characters that are silently deleted by one or more browsers + cx = /[\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/; + + // token + tx = /^\s*([(){}\[.,:;'"~\?\]#@]|==?=?|\/(\*(jshint|jslint|members?|global)?|=|\/)?|\*[\/=]?|\+(?:=|\++)?|-(?:=|-+)?|%=?|&[&=]?|\|[|=]?|>>?>?=?|<([\/=!]|\!(\[|--)?|<=?)?|\^=?|\!=?=?|[a-zA-Z_$][a-zA-Z0-9_$]*|[0-9]+([xX][0-9a-fA-F]+|\.[0-9]*)?([eE][+\-]?[0-9]+)?)/; + + // characters in strings that need escapement + nx = /[\u0000-\u001f&<"\/\\\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/; + nxg = /[\u0000-\u001f&<"\/\\\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + + // star slash + lx = /\*\/|\/\*/; + + // identifier + ix = /^([a-zA-Z_$][a-zA-Z0-9_$]*)$/; + + // javascript url + jx = /^(?:javascript|jscript|ecmascript|vbscript|mocha|livescript)\s*:/i; + + // catches /* falls through */ comments + ft = /^\s*\/\*\s*falls\sthrough\s*\*\/\s*$/; + }()); + + function F() {} // Used by Object.create + + function is_own(object, name) { + +// The object.hasOwnProperty method fails when the property under consideration +// is named 'hasOwnProperty'. So we have to use this more convoluted form. + + return Object.prototype.hasOwnProperty.call(object, name); + } + +// Provide critical ES5 functions to ES3. + + if (typeof Array.isArray !== 'function') { + Array.isArray = function (o) { + return Object.prototype.toString.apply(o) === '[object Array]'; + }; + } + + if (typeof Object.create !== 'function') { + Object.create = function (o) { + F.prototype = o; + return new F(); + }; + } + + if (typeof Object.keys !== 'function') { + Object.keys = function (o) { + var a = [], k; + for (k in o) { + if (is_own(o, k)) { + a.push(k); + } + } + return a; + }; + } + +// Non standard methods + + if (typeof String.prototype.entityify !== 'function') { + String.prototype.entityify = function () { + return this + .replace(/&/g, '&') + .replace(//g, '>'); + }; + } + + if (typeof String.prototype.isAlpha !== 'function') { + String.prototype.isAlpha = function () { + return (this >= 'a' && this <= 'z\uffff') || + (this >= 'A' && this <= 'Z\uffff'); + }; + } + + if (typeof String.prototype.isDigit !== 'function') { + String.prototype.isDigit = function () { + return (this >= '0' && this <= '9'); + }; + } + + if (typeof String.prototype.supplant !== 'function') { + String.prototype.supplant = function (o) { + return this.replace(/\{([^{}]*)\}/g, function (a, b) { + var r = o[b]; + return typeof r === 'string' || typeof r === 'number' ? r : a; + }); + }; + } + + if (typeof String.prototype.name !== 'function') { + String.prototype.name = function () { + +// If the string looks like an identifier, then we can return it as is. +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can simply slap some quotes around it. +// Otherwise we must also replace the offending characters with safe +// sequences. + + if (ix.test(this)) { + return this; + } + if (nx.test(this)) { + return '"' + this.replace(nxg, function (a) { + var c = escapes[a]; + if (c) { + return c; + } + return '\\u' + ('0000' + a.charCodeAt().toString(16)).slice(-4); + }) + '"'; + } + return '"' + this + '"'; + }; + } + + + function combine(t, o) { + var n; + for (n in o) { + if (is_own(o, n)) { + t[n] = o[n]; + } + } + } + + function assume() { + if (option.couch) { + combine(predefined, couch); + } + + if (option.rhino) { + combine(predefined, rhino); + } + + if (option.prototypejs) { + combine(predefined, prototypejs); + } + + if (option.node) { + combine(predefined, node); + } + + if (option.devel) { + combine(predefined, devel); + } + + if (option.dojo) { + combine(predefined, dojo); + } + + if (option.browser) { + combine(predefined, browser); + } + + if (option.nonstandard) { + combine(predefined, nonstandard); + } + + if (option.jquery) { + combine(predefined, jquery); + } + + if (option.mootools) { + combine(predefined, mootools); + } + + if (option.wsh) { + combine(predefined, wsh); + } + + if (option.esnext) { + useESNextSyntax(); + } + + if (option.globalstrict && option.strict !== false) { + option.strict = true; + } + } + + + // Produce an error warning. + function quit(message, line, chr) { + var percentage = Math.floor((line / lines.length) * 100); + + throw { + name: 'JSHintError', + line: line, + character: chr, + message: message + " (" + percentage + "% scanned).", + raw: message + }; + } + + function isundef(scope, m, t, a) { + return JSHINT.undefs.push([scope, m, t, a]); + } + + function warning(m, t, a, b, c, d) { + var ch, l, w; + t = t || nexttoken; + if (t.id === '(end)') { // `~ + t = token; + } + l = t.line || 0; + ch = t.from || 0; + w = { + id: '(error)', + raw: m, + evidence: lines[l - 1] || '', + line: l, + character: ch, + a: a, + b: b, + c: c, + d: d + }; + w.reason = m.supplant(w); + JSHINT.errors.push(w); + if (option.passfail) { + quit('Stopping. ', l, ch); + } + warnings += 1; + if (warnings >= option.maxerr) { + quit("Too many errors.", l, ch); + } + return w; + } + + function warningAt(m, l, ch, a, b, c, d) { + return warning(m, { + line: l, + from: ch + }, a, b, c, d); + } + + function error(m, t, a, b, c, d) { + var w = warning(m, t, a, b, c, d); + } + + function errorAt(m, l, ch, a, b, c, d) { + return error(m, { + line: l, + from: ch + }, a, b, c, d); + } + + + +// lexical analysis and token construction + + var lex = (function lex() { + var character, from, line, s; + +// Private lex methods + + function nextLine() { + var at, + tw; // trailing whitespace check + + if (line >= lines.length) + return false; + + character = 1; + s = lines[line]; + line += 1; + + // If smarttabs option is used check for spaces followed by tabs only. + // Otherwise check for any occurence of mixed tabs and spaces. + if (option.smarttabs) + at = s.search(/ \t/); + else + at = s.search(/ \t|\t /); + + if (at >= 0) + warningAt("Mixed spaces and tabs.", line, at + 1); + + s = s.replace(/\t/g, tab); + at = s.search(cx); + + if (at >= 0) + warningAt("Unsafe character.", line, at); + + if (option.maxlen && option.maxlen < s.length) + warningAt("Line too long.", line, s.length); + + // Check for trailing whitespaces + tw = /\s+$/.test(s); + if (option.trailing && tw && !/^\s+$/.test(s)) { + warningAt("Trailing whitespace.", line, tw); + } + return true; + } + +// Produce a token object. The token inherits from a syntax symbol. + + function it(type, value) { + var i, t; + if (type === '(color)' || type === '(range)') { + t = {type: type}; + } else if (type === '(punctuator)' || + (type === '(identifier)' && is_own(syntax, value))) { + t = syntax[value] || syntax['(error)']; + } else { + t = syntax[type]; + } + t = Object.create(t); + if (type === '(string)' || type === '(range)') { + if (!option.scripturl && jx.test(value)) { + warningAt("Script URL.", line, from); + } + } + if (type === '(identifier)') { + t.identifier = true; + if (value === '__proto__' && !option.proto) { + warningAt("The '{a}' property is deprecated.", + line, from, value); + } else if (value === '__iterator__' && !option.iterator) { + warningAt("'{a}' is only available in JavaScript 1.7.", + line, from, value); + } else if (option.nomen && (value.charAt(0) === '_' || + value.charAt(value.length - 1) === '_')) { + if (!option.node || token.id === '.' || + (value !== '__dirname' && value !== '__filename')) { + warningAt("Unexpected {a} in '{b}'.", line, from, "dangling '_'", value); + } + } + } + t.value = value; + t.line = line; + t.character = character; + t.from = from; + i = t.id; + if (i !== '(endline)') { + prereg = i && + (('(,=:[!&|?{};'.indexOf(i.charAt(i.length - 1)) >= 0) || + i === 'return' || + i === 'case'); + } + return t; + } + + // Public lex methods + return { + init: function (source) { + if (typeof source === 'string') { + lines = source + .replace(/\r\n/g, '\n') + .replace(/\r/g, '\n') + .split('\n'); + } else { + lines = source; + } + + // If the first line is a shebang (#!), make it a blank and move on. + // Shebangs are used by Node scripts. + if (lines[0] && lines[0].substr(0, 2) === '#!') + lines[0] = ''; + + line = 0; + nextLine(); + from = 1; + }, + + range: function (begin, end) { + var c, value = ''; + from = character; + if (s.charAt(0) !== begin) { + errorAt("Expected '{a}' and instead saw '{b}'.", + line, character, begin, s.charAt(0)); + } + for (;;) { + s = s.slice(1); + character += 1; + c = s.charAt(0); + switch (c) { + case '': + errorAt("Missing '{a}'.", line, character, c); + break; + case end: + s = s.slice(1); + character += 1; + return it('(range)', value); + case '\\': + warningAt("Unexpected '{a}'.", line, character, c); + } + value += c; + } + + }, + + + // token -- this is called by advance to get the next token + token: function () { + var b, c, captures, d, depth, high, i, l, low, q, t, isLiteral, isInRange, n; + + function match(x) { + var r = x.exec(s), r1; + if (r) { + l = r[0].length; + r1 = r[1]; + c = r1.charAt(0); + s = s.substr(l); + from = character + l - r1.length; + character += l; + return r1; + } + } + + function string(x) { + var c, j, r = '', allowNewLine = false; + + if (jsonmode && x !== '"') { + warningAt("Strings must use doublequote.", + line, character); + } + + function esc(n) { + var i = parseInt(s.substr(j + 1, n), 16); + j += n; + if (i >= 32 && i <= 126 && + i !== 34 && i !== 92 && i !== 39) { + warningAt("Unnecessary escapement.", line, character); + } + character += n; + c = String.fromCharCode(i); + } + j = 0; +unclosedString: for (;;) { + while (j >= s.length) { + j = 0; + + var cl = line, cf = from; + if (!nextLine()) { + errorAt("Unclosed string.", cl, cf); + break unclosedString; + } + + if (allowNewLine) { + allowNewLine = false; + } else { + warningAt("Unclosed string.", cl, cf); + } + } + c = s.charAt(j); + if (c === x) { + character += 1; + s = s.substr(j + 1); + return it('(string)', r, x); + } + if (c < ' ') { + if (c === '\n' || c === '\r') { + break; + } + warningAt("Control character in string: {a}.", + line, character + j, s.slice(0, j)); + } else if (c === '\\') { + j += 1; + character += 1; + c = s.charAt(j); + n = s.charAt(j + 1); + switch (c) { + case '\\': + case '"': + case '/': + break; + case '\'': + if (jsonmode) { + warningAt("Avoid \\'.", line, character); + } + break; + case 'b': + c = '\b'; + break; + case 'f': + c = '\f'; + break; + case 'n': + c = '\n'; + break; + case 'r': + c = '\r'; + break; + case 't': + c = '\t'; + break; + case '0': + c = '\0'; + // Octal literals fail in strict mode + // check if the number is between 00 and 07 + // where 'n' is the token next to 'c' + if (n >= 0 && n <= 7 && directive["use strict"]) { + warningAt( + "Octal literals are not allowed in strict mode.", + line, character); + } + break; + case 'u': + esc(4); + break; + case 'v': + if (jsonmode) { + warningAt("Avoid \\v.", line, character); + } + c = '\v'; + break; + case 'x': + if (jsonmode) { + warningAt("Avoid \\x-.", line, character); + } + esc(2); + break; + case '': + // last character is escape character + // always allow new line if escaped, but show + // warning if option is not set + allowNewLine = true; + if (option.multistr) { + if (jsonmode) { + warningAt("Avoid EOL escapement.", line, character); + } + c = ''; + character -= 1; + break; + } + warningAt("Bad escapement of EOL. Use option multistr if needed.", + line, character); + break; + default: + warningAt("Bad escapement.", line, character); + } + } + r += c; + character += 1; + j += 1; + } + } + + for (;;) { + if (!s) { + return it(nextLine() ? '(endline)' : '(end)', ''); + } + t = match(tx); + if (!t) { + t = ''; + c = ''; + while (s && s < '!') { + s = s.substr(1); + } + if (s) { + errorAt("Unexpected '{a}'.", line, character, s.substr(0, 1)); + s = ''; + } + } else { + + // identifier + + if (c.isAlpha() || c === '_' || c === '$') { + return it('(identifier)', t); + } + + // number + + if (c.isDigit()) { + if (!isFinite(Number(t))) { + warningAt("Bad number '{a}'.", + line, character, t); + } + if (s.substr(0, 1).isAlpha()) { + warningAt("Missing space after '{a}'.", + line, character, t); + } + if (c === '0') { + d = t.substr(1, 1); + if (d.isDigit()) { + if (token.id !== '.') { + warningAt("Don't use extra leading zeros '{a}'.", + line, character, t); + } + } else if (jsonmode && (d === 'x' || d === 'X')) { + warningAt("Avoid 0x-. '{a}'.", + line, character, t); + } + } + if (t.substr(t.length - 1) === '.') { + warningAt( +"A trailing decimal point can be confused with a dot '{a}'.", line, character, t); + } + return it('(number)', t); + } + switch (t) { + + // string + + case '"': + case "'": + return string(t); + + // // comment + + case '//': + s = ''; + token.comment = true; + break; + + // /* comment + + case '/*': + for (;;) { + i = s.search(lx); + if (i >= 0) { + break; + } + if (!nextLine()) { + errorAt("Unclosed comment.", line, character); + } + } + character += i + 2; + if (s.substr(i, 1) === '/') { + errorAt("Nested comment.", line, character); + } + s = s.substr(i + 2); + token.comment = true; + break; + + // /*members /*jshint /*global + + case '/*members': + case '/*member': + case '/*jshint': + case '/*jslint': + case '/*global': + case '*/': + return { + value: t, + type: 'special', + line: line, + character: character, + from: from + }; + + case '': + break; + // / + case '/': + if (token.id === '/=') { + errorAt("A regular expression literal can be confused with '/='.", + line, from); + } + if (prereg) { + depth = 0; + captures = 0; + l = 0; + for (;;) { + b = true; + c = s.charAt(l); + l += 1; + switch (c) { + case '': + errorAt("Unclosed regular expression.", line, from); + return quit('Stopping.', line, from); + case '/': + if (depth > 0) { + warningAt("{a} unterminated regular expression " + + "group(s).", line, from + l, depth); + } + c = s.substr(0, l - 1); + q = { + g: true, + i: true, + m: true + }; + while (q[s.charAt(l)] === true) { + q[s.charAt(l)] = false; + l += 1; + } + character += l; + s = s.substr(l); + q = s.charAt(0); + if (q === '/' || q === '*') { + errorAt("Confusing regular expression.", + line, from); + } + return it('(regexp)', c); + case '\\': + c = s.charAt(l); + if (c < ' ') { + warningAt( +"Unexpected control character in regular expression.", line, from + l); + } else if (c === '<') { + warningAt( +"Unexpected escaped character '{a}' in regular expression.", line, from + l, c); + } + l += 1; + break; + case '(': + depth += 1; + b = false; + if (s.charAt(l) === '?') { + l += 1; + switch (s.charAt(l)) { + case ':': + case '=': + case '!': + l += 1; + break; + default: + warningAt( +"Expected '{a}' and instead saw '{b}'.", line, from + l, ':', s.charAt(l)); + } + } else { + captures += 1; + } + break; + case '|': + b = false; + break; + case ')': + if (depth === 0) { + warningAt("Unescaped '{a}'.", + line, from + l, ')'); + } else { + depth -= 1; + } + break; + case ' ': + q = 1; + while (s.charAt(l) === ' ') { + l += 1; + q += 1; + } + if (q > 1) { + warningAt( +"Spaces are hard to count. Use {{a}}.", line, from + l, q); + } + break; + case '[': + c = s.charAt(l); + if (c === '^') { + l += 1; + if (option.regexp) { + warningAt("Insecure '{a}'.", + line, from + l, c); + } else if (s.charAt(l) === ']') { + errorAt("Unescaped '{a}'.", + line, from + l, '^'); + } + } + if (c === ']') { + warningAt("Empty class.", line, + from + l - 1); + } + isLiteral = false; + isInRange = false; +klass: do { + c = s.charAt(l); + l += 1; + switch (c) { + case '[': + case '^': + warningAt("Unescaped '{a}'.", + line, from + l, c); + if (isInRange) { + isInRange = false; + } else { + isLiteral = true; + } + break; + case '-': + if (isLiteral && !isInRange) { + isLiteral = false; + isInRange = true; + } else if (isInRange) { + isInRange = false; + } else if (s.charAt(l) === ']') { + isInRange = true; + } else { + if (option.regexdash !== (l === 2 || (l === 3 && + s.charAt(1) === '^'))) { + warningAt("Unescaped '{a}'.", + line, from + l - 1, '-'); + } + isLiteral = true; + } + break; + case ']': + if (isInRange && !option.regexdash) { + warningAt("Unescaped '{a}'.", + line, from + l - 1, '-'); + } + break klass; + case '\\': + c = s.charAt(l); + if (c < ' ') { + warningAt( +"Unexpected control character in regular expression.", line, from + l); + } else if (c === '<') { + warningAt( +"Unexpected escaped character '{a}' in regular expression.", line, from + l, c); + } + l += 1; + + // \w, \s and \d are never part of a character range + if (/[wsd]/i.test(c)) { + if (isInRange) { + warningAt("Unescaped '{a}'.", + line, from + l, '-'); + isInRange = false; + } + isLiteral = false; + } else if (isInRange) { + isInRange = false; + } else { + isLiteral = true; + } + break; + case '/': + warningAt("Unescaped '{a}'.", + line, from + l - 1, '/'); + + if (isInRange) { + isInRange = false; + } else { + isLiteral = true; + } + break; + case '<': + if (isInRange) { + isInRange = false; + } else { + isLiteral = true; + } + break; + default: + if (isInRange) { + isInRange = false; + } else { + isLiteral = true; + } + } + } while (c); + break; + case '.': + if (option.regexp) { + warningAt("Insecure '{a}'.", line, + from + l, c); + } + break; + case ']': + case '?': + case '{': + case '}': + case '+': + case '*': + warningAt("Unescaped '{a}'.", line, + from + l, c); + } + if (b) { + switch (s.charAt(l)) { + case '?': + case '+': + case '*': + l += 1; + if (s.charAt(l) === '?') { + l += 1; + } + break; + case '{': + l += 1; + c = s.charAt(l); + if (c < '0' || c > '9') { + warningAt( +"Expected a number and instead saw '{a}'.", line, from + l, c); + } + l += 1; + low = +c; + for (;;) { + c = s.charAt(l); + if (c < '0' || c > '9') { + break; + } + l += 1; + low = +c + (low * 10); + } + high = low; + if (c === ',') { + l += 1; + high = Infinity; + c = s.charAt(l); + if (c >= '0' && c <= '9') { + l += 1; + high = +c; + for (;;) { + c = s.charAt(l); + if (c < '0' || c > '9') { + break; + } + l += 1; + high = +c + (high * 10); + } + } + } + if (s.charAt(l) !== '}') { + warningAt( +"Expected '{a}' and instead saw '{b}'.", line, from + l, '}', c); + } else { + l += 1; + } + if (s.charAt(l) === '?') { + l += 1; + } + if (low > high) { + warningAt( +"'{a}' should not be greater than '{b}'.", line, from + l, low, high); + } + } + } + } + c = s.substr(0, l - 1); + character += l; + s = s.substr(l); + return it('(regexp)', c); + } + return it('(punctuator)', t); + + // punctuator + + case '#': + return it('(punctuator)', t); + default: + return it('(punctuator)', t); + } + } + } + } + }; + }()); + + + function addlabel(t, type) { + + if (t === 'hasOwnProperty') { + warning("'hasOwnProperty' is a really bad name."); + } + +// Define t in the current function in the current scope. + if (is_own(funct, t) && !funct['(global)']) { + if (funct[t] === true) { + if (option.latedef) + warning("'{a}' was used before it was defined.", nexttoken, t); + } else { + if (!option.shadow && type !== "exception") + warning("'{a}' is already defined.", nexttoken, t); + } + } + + funct[t] = type; + if (funct['(global)']) { + global[t] = funct; + if (is_own(implied, t)) { + if (option.latedef) + warning("'{a}' was used before it was defined.", nexttoken, t); + delete implied[t]; + } + } else { + scope[t] = funct; + } + } + + + function doOption() { + var b, obj, filter, o = nexttoken.value, t, v; + switch (o) { + case '*/': + error("Unbegun comment."); + break; + case '/*members': + case '/*member': + o = '/*members'; + if (!membersOnly) { + membersOnly = {}; + } + obj = membersOnly; + break; + case '/*jshint': + case '/*jslint': + obj = option; + filter = boolOptions; + break; + case '/*global': + obj = predefined; + break; + default: + error("What?"); + } + t = lex.token(); +loop: for (;;) { + for (;;) { + if (t.type === 'special' && t.value === '*/') { + break loop; + } + if (t.id !== '(endline)' && t.id !== ',') { + break; + } + t = lex.token(); + } + if (t.type !== '(string)' && t.type !== '(identifier)' && + o !== '/*members') { + error("Bad option.", t); + } + v = lex.token(); + if (v.id === ':') { + v = lex.token(); + if (obj === membersOnly) { + error("Expected '{a}' and instead saw '{b}'.", + t, '*/', ':'); + } + if (t.value === 'indent' && (o === '/*jshint' || o === '/*jslint')) { + b = +v.value; + if (typeof b !== 'number' || !isFinite(b) || b <= 0 || + Math.floor(b) !== b) { + error("Expected a small integer and instead saw '{a}'.", + v, v.value); + } + obj.white = true; + obj.indent = b; + } else if (t.value === 'maxerr' && (o === '/*jshint' || o === '/*jslint')) { + b = +v.value; + if (typeof b !== 'number' || !isFinite(b) || b <= 0 || + Math.floor(b) !== b) { + error("Expected a small integer and instead saw '{a}'.", + v, v.value); + } + obj.maxerr = b; + } else if (t.value === 'maxlen' && (o === '/*jshint' || o === '/*jslint')) { + b = +v.value; + if (typeof b !== 'number' || !isFinite(b) || b <= 0 || + Math.floor(b) !== b) { + error("Expected a small integer and instead saw '{a}'.", + v, v.value); + } + obj.maxlen = b; + } else if (t.value === 'validthis') { + if (funct['(global)']) { + error("Option 'validthis' can't be used in a global scope."); + } else { + if (v.value === 'true' || v.value === 'false') + obj[t.value] = v.value === 'true'; + else + error("Bad option value.", v); + } + } else if (v.value === 'true') { + obj[t.value] = true; + } else if (v.value === 'false') { + obj[t.value] = false; + } else { + error("Bad option value.", v); + } + t = lex.token(); + } else { + if (o === '/*jshint' || o === '/*jslint') { + error("Missing option value.", t); + } + obj[t.value] = false; + t = v; + } + } + if (filter) { + assume(); + } + } + + +// We need a peek function. If it has an argument, it peeks that much farther +// ahead. It is used to distinguish +// for ( var i in ... +// from +// for ( var i = ... + + function peek(p) { + var i = p || 0, j = 0, t; + + while (j <= i) { + t = lookahead[j]; + if (!t) { + t = lookahead[j] = lex.token(); + } + j += 1; + } + return t; + } + + + +// Produce the next token. It looks for programming errors. + + function advance(id, t) { + switch (token.id) { + case '(number)': + if (nexttoken.id === '.') { + warning("A dot following a number can be confused with a decimal point.", token); + } + break; + case '-': + if (nexttoken.id === '-' || nexttoken.id === '--') { + warning("Confusing minusses."); + } + break; + case '+': + if (nexttoken.id === '+' || nexttoken.id === '++') { + warning("Confusing plusses."); + } + break; + } + + if (token.type === '(string)' || token.identifier) { + anonname = token.value; + } + + if (id && nexttoken.id !== id) { + if (t) { + if (nexttoken.id === '(end)') { + warning("Unmatched '{a}'.", t, t.id); + } else { + warning("Expected '{a}' to match '{b}' from line {c} and instead saw '{d}'.", + nexttoken, id, t.id, t.line, nexttoken.value); + } + } else if (nexttoken.type !== '(identifier)' || + nexttoken.value !== id) { + warning("Expected '{a}' and instead saw '{b}'.", + nexttoken, id, nexttoken.value); + } + } + + prevtoken = token; + token = nexttoken; + for (;;) { + nexttoken = lookahead.shift() || lex.token(); + if (nexttoken.id === '(end)' || nexttoken.id === '(error)') { + return; + } + if (nexttoken.type === 'special') { + doOption(); + } else { + if (nexttoken.id !== '(endline)') { + break; + } + } + } + } + + +// This is the heart of JSHINT, the Pratt parser. In addition to parsing, it +// is looking for ad hoc lint patterns. We add .fud to Pratt's model, which is +// like .nud except that it is only used on the first token of a statement. +// Having .fud makes it much easier to define statement-oriented languages like +// JavaScript. I retained Pratt's nomenclature. + +// .nud Null denotation +// .fud First null denotation +// .led Left denotation +// lbp Left binding power +// rbp Right binding power + +// They are elements of the parsing method called Top Down Operator Precedence. + + function expression(rbp, initial) { + var left, isArray = false; + + if (nexttoken.id === '(end)') + error("Unexpected early end of program.", token); + + advance(); + if (initial) { + anonname = 'anonymous'; + funct['(verb)'] = token.value; + } + if (initial === true && token.fud) { + left = token.fud(); + } else { + if (token.nud) { + left = token.nud(); + } else { + if (nexttoken.type === '(number)' && token.id === '.') { + warning("A leading decimal point can be confused with a dot: '.{a}'.", + token, nexttoken.value); + advance(); + return token; + } else { + error("Expected an identifier and instead saw '{a}'.", + token, token.id); + } + } + while (rbp < nexttoken.lbp) { + isArray = token.value === 'Array'; + advance(); + if (isArray && token.id === '(' && nexttoken.id === ')') + warning("Use the array literal notation [].", token); + if (token.led) { + left = token.led(left); + } else { + error("Expected an operator and instead saw '{a}'.", + token, token.id); + } + } + } + return left; + } + + +// Functions for conformance of style. + + function adjacent(left, right) { + left = left || token; + right = right || nexttoken; + if (option.white) { + if (left.character !== right.from && left.line === right.line) { + left.from += (left.character - left.from); + warning("Unexpected space after '{a}'.", left, left.value); + } + } + } + + function nobreak(left, right) { + left = left || token; + right = right || nexttoken; + if (option.white && (left.character !== right.from || left.line !== right.line)) { + warning("Unexpected space before '{a}'.", right, right.value); + } + } + + function nospace(left, right) { + left = left || token; + right = right || nexttoken; + if (option.white && !left.comment) { + if (left.line === right.line) { + adjacent(left, right); + } + } + } + + function nonadjacent(left, right) { + if (option.white) { + left = left || token; + right = right || nexttoken; + if (left.line === right.line && left.character === right.from) { + left.from += (left.character - left.from); + warning("Missing space after '{a}'.", + left, left.value); + } + } + } + + function nobreaknonadjacent(left, right) { + left = left || token; + right = right || nexttoken; + if (!option.laxbreak && left.line !== right.line) { + warning("Bad line breaking before '{a}'.", right, right.id); + } else if (option.white) { + left = left || token; + right = right || nexttoken; + if (left.character === right.from) { + left.from += (left.character - left.from); + warning("Missing space after '{a}'.", + left, left.value); + } + } + } + + function indentation(bias) { + var i; + if (option.white && nexttoken.id !== '(end)') { + i = indent + (bias || 0); + if (nexttoken.from !== i) { + warning( +"Expected '{a}' to have an indentation at {b} instead at {c}.", + nexttoken, nexttoken.value, i, nexttoken.from); + } + } + } + + function nolinebreak(t) { + t = t || token; + if (t.line !== nexttoken.line) { + warning("Line breaking error '{a}'.", t, t.value); + } + } + + + function comma() { + if (token.line !== nexttoken.line) { + if (!option.laxcomma) { + if (comma.first) { + warning("Comma warnings can be turned off with 'laxcomma'"); + comma.first = false; + } + warning("Bad line breaking before '{a}'.", token, nexttoken.id); + } + } else if (!token.comment && token.character !== nexttoken.from && option.white) { + token.from += (token.character - token.from); + warning("Unexpected space after '{a}'.", token, token.value); + } + advance(','); + nonadjacent(token, nexttoken); + } + + +// Functional constructors for making the symbols that will be inherited by +// tokens. + + function symbol(s, p) { + var x = syntax[s]; + if (!x || typeof x !== 'object') { + syntax[s] = x = { + id: s, + lbp: p, + value: s + }; + } + return x; + } + + + function delim(s) { + return symbol(s, 0); + } + + + function stmt(s, f) { + var x = delim(s); + x.identifier = x.reserved = true; + x.fud = f; + return x; + } + + + function blockstmt(s, f) { + var x = stmt(s, f); + x.block = true; + return x; + } + + + function reserveName(x) { + var c = x.id.charAt(0); + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { + x.identifier = x.reserved = true; + } + return x; + } + + + function prefix(s, f) { + var x = symbol(s, 150); + reserveName(x); + x.nud = (typeof f === 'function') ? f : function () { + this.right = expression(150); + this.arity = 'unary'; + if (this.id === '++' || this.id === '--') { + if (option.plusplus) { + warning("Unexpected use of '{a}'.", this, this.id); + } else if ((!this.right.identifier || this.right.reserved) && + this.right.id !== '.' && this.right.id !== '[') { + warning("Bad operand.", this); + } + } + return this; + }; + return x; + } + + + function type(s, f) { + var x = delim(s); + x.type = s; + x.nud = f; + return x; + } + + + function reserve(s, f) { + var x = type(s, f); + x.identifier = x.reserved = true; + return x; + } + + + function reservevar(s, v) { + return reserve(s, function () { + if (typeof v === 'function') { + v(this); + } + return this; + }); + } + + + function infix(s, f, p, w) { + var x = symbol(s, p); + reserveName(x); + x.led = function (left) { + if (!w) { + nobreaknonadjacent(prevtoken, token); + nonadjacent(token, nexttoken); + } + if (s === "in" && left.id === "!") { + warning("Confusing use of '{a}'.", left, '!'); + } + if (typeof f === 'function') { + return f(left, this); + } else { + this.left = left; + this.right = expression(p); + return this; + } + }; + return x; + } + + + function relation(s, f) { + var x = symbol(s, 100); + x.led = function (left) { + nobreaknonadjacent(prevtoken, token); + nonadjacent(token, nexttoken); + var right = expression(100); + if ((left && left.id === 'NaN') || (right && right.id === 'NaN')) { + warning("Use the isNaN function to compare with NaN.", this); + } else if (f) { + f.apply(this, [left, right]); + } + if (left.id === '!') { + warning("Confusing use of '{a}'.", left, '!'); + } + if (right.id === '!') { + warning("Confusing use of '{a}'.", right, '!'); + } + this.left = left; + this.right = right; + return this; + }; + return x; + } + + + function isPoorRelation(node) { + return node && + ((node.type === '(number)' && +node.value === 0) || + (node.type === '(string)' && node.value === '') || + (node.type === 'null' && !option.eqnull) || + node.type === 'true' || + node.type === 'false' || + node.type === 'undefined'); + } + + + function assignop(s, f) { + symbol(s, 20).exps = true; + return infix(s, function (left, that) { + var l; + that.left = left; + if (predefined[left.value] === false && + scope[left.value]['(global)'] === true) { + warning("Read only.", left); + } else if (left['function']) { + warning("'{a}' is a function.", left, left.value); + } + if (left) { + if (option.esnext && funct[left.value] === 'const') { + warning("Attempting to override '{a}' which is a constant", left, left.value); + } + if (left.id === '.' || left.id === '[') { + if (!left.left || left.left.value === 'arguments') { + warning('Bad assignment.', that); + } + that.right = expression(19); + return that; + } else if (left.identifier && !left.reserved) { + if (funct[left.value] === 'exception') { + warning("Do not assign to the exception parameter.", left); + } + that.right = expression(19); + return that; + } + if (left === syntax['function']) { + warning( +"Expected an identifier in an assignment and instead saw a function invocation.", + token); + } + } + error("Bad assignment.", that); + }, 20); + } + + + function bitwise(s, f, p) { + var x = symbol(s, p); + reserveName(x); + x.led = (typeof f === 'function') ? f : function (left) { + if (option.bitwise) { + warning("Unexpected use of '{a}'.", this, this.id); + } + this.left = left; + this.right = expression(p); + return this; + }; + return x; + } + + + function bitwiseassignop(s) { + symbol(s, 20).exps = true; + return infix(s, function (left, that) { + if (option.bitwise) { + warning("Unexpected use of '{a}'.", that, that.id); + } + nonadjacent(prevtoken, token); + nonadjacent(token, nexttoken); + if (left) { + if (left.id === '.' || left.id === '[' || + (left.identifier && !left.reserved)) { + expression(19); + return that; + } + if (left === syntax['function']) { + warning( +"Expected an identifier in an assignment, and instead saw a function invocation.", + token); + } + return that; + } + error("Bad assignment.", that); + }, 20); + } + + + function suffix(s, f) { + var x = symbol(s, 150); + x.led = function (left) { + if (option.plusplus) { + warning("Unexpected use of '{a}'.", this, this.id); + } else if ((!left.identifier || left.reserved) && + left.id !== '.' && left.id !== '[') { + warning("Bad operand.", this); + } + this.left = left; + return this; + }; + return x; + } + + + // fnparam means that this identifier is being defined as a function + // argument (see identifier()) + function optionalidentifier(fnparam) { + if (nexttoken.identifier) { + advance(); + if (token.reserved && !option.es5) { + // `undefined` as a function param is a common pattern to protect + // against the case when somebody does `undefined = true` and + // help with minification. More info: https://gist.github.com/315916 + if (!fnparam || token.value !== 'undefined') { + warning("Expected an identifier and instead saw '{a}' (a reserved word).", + token, token.id); + } + } + return token.value; + } + } + + // fnparam means that this identifier is being defined as a function + // argument + function identifier(fnparam) { + var i = optionalidentifier(fnparam); + if (i) { + return i; + } + if (token.id === 'function' && nexttoken.id === '(') { + warning("Missing name in function declaration."); + } else { + error("Expected an identifier and instead saw '{a}'.", + nexttoken, nexttoken.value); + } + } + + + function reachable(s) { + var i = 0, t; + if (nexttoken.id !== ';' || noreach) { + return; + } + for (;;) { + t = peek(i); + if (t.reach) { + return; + } + if (t.id !== '(endline)') { + if (t.id === 'function') { + if (!option.latedef) { + break; + } + warning( +"Inner functions should be listed at the top of the outer function.", t); + break; + } + warning("Unreachable '{a}' after '{b}'.", t, t.value, s); + break; + } + i += 1; + } + } + + + function statement(noindent) { + var i = indent, r, s = scope, t = nexttoken; + + if (t.id === ";") { + advance(";"); + return; + } + +// Is this a labelled statement? + + if (t.identifier && !t.reserved && peek().id === ':') { + advance(); + advance(':'); + scope = Object.create(s); + addlabel(t.value, 'label'); + if (!nexttoken.labelled) { + warning("Label '{a}' on {b} statement.", + nexttoken, t.value, nexttoken.value); + } + if (jx.test(t.value + ':')) { + warning("Label '{a}' looks like a javascript url.", + t, t.value); + } + nexttoken.label = t.value; + t = nexttoken; + } + +// Parse the statement. + + if (!noindent) { + indentation(); + } + r = expression(0, true); + + // Look for the final semicolon. + if (!t.block) { + if (!option.expr && (!r || !r.exps)) { + warning("Expected an assignment or function call and instead saw an expression.", + token); + } else if (option.nonew && r.id === '(' && r.left.id === 'new') { + warning("Do not use 'new' for side effects."); + } + + if (nexttoken.id === ',') { + return comma(); + } + + if (nexttoken.id !== ';') { + if (!option.asi) { + // If this is the last statement in a block that ends on + // the same line *and* option lastsemic is on, ignore the warning. + // Otherwise, complain about missing semicolon. + if (!option.lastsemic || nexttoken.id !== '}' || + nexttoken.line !== token.line) { + warningAt("Missing semicolon.", token.line, token.character); + } + } + } else { + adjacent(token, nexttoken); + advance(';'); + nonadjacent(token, nexttoken); + } + } + +// Restore the indentation. + + indent = i; + scope = s; + return r; + } + + + function statements(startLine) { + var a = [], f, p; + + while (!nexttoken.reach && nexttoken.id !== '(end)') { + if (nexttoken.id === ';') { + p = peek(); + if (!p || p.id !== "(") { + warning("Unnecessary semicolon."); + } + advance(';'); + } else { + a.push(statement(startLine === nexttoken.line)); + } + } + return a; + } + + + /* + * read all directives + * recognizes a simple form of asi, but always + * warns, if it is used + */ + function directives() { + var i, p, pn; + + for (;;) { + if (nexttoken.id === "(string)") { + p = peek(0); + if (p.id === "(endline)") { + i = 1; + do { + pn = peek(i); + i = i + 1; + } while (pn.id === "(endline)"); + + if (pn.id !== ";") { + if (pn.id !== "(string)" && pn.id !== "(number)" && + pn.id !== "(regexp)" && pn.identifier !== true && + pn.id !== "}") { + break; + } + warning("Missing semicolon.", nexttoken); + } else { + p = pn; + } + } else if (p.id === "}") { + // directive with no other statements, warn about missing semicolon + warning("Missing semicolon.", p); + } else if (p.id !== ";") { + break; + } + + indentation(); + advance(); + if (directive[token.value]) { + warning("Unnecessary directive \"{a}\".", token, token.value); + } + + if (token.value === "use strict") { + option.newcap = true; + option.undef = true; + } + + // there's no directive negation, so always set to true + directive[token.value] = true; + + if (p.id === ";") { + advance(";"); + } + continue; + } + break; + } + } + + + /* + * Parses a single block. A block is a sequence of statements wrapped in + * braces. + * + * ordinary - true for everything but function bodies and try blocks. + * stmt - true if block can be a single statement (e.g. in if/for/while). + * isfunc - true if block is a function body + */ + function block(ordinary, stmt, isfunc) { + var a, + b = inblock, + old_indent = indent, + m, + s = scope, + t, + line, + d; + + inblock = ordinary; + if (!ordinary || !option.funcscope) scope = Object.create(scope); + nonadjacent(token, nexttoken); + t = nexttoken; + + if (nexttoken.id === '{') { + advance('{'); + line = token.line; + if (nexttoken.id !== '}') { + indent += option.indent; + while (!ordinary && nexttoken.from > indent) { + indent += option.indent; + } + + if (isfunc) { + m = {}; + for (d in directive) { + if (is_own(directive, d)) { + m[d] = directive[d]; + } + } + directives(); + + if (option.strict && funct['(context)']['(global)']) { + if (!m["use strict"] && !directive["use strict"]) { + warning("Missing \"use strict\" statement."); + } + } + } + + a = statements(line); + + if (isfunc) { + directive = m; + } + + indent -= option.indent; + if (line !== nexttoken.line) { + indentation(); + } + } else if (line !== nexttoken.line) { + indentation(); + } + advance('}', t); + indent = old_indent; + } else if (!ordinary) { + error("Expected '{a}' and instead saw '{b}'.", + nexttoken, '{', nexttoken.value); + } else { + if (!stmt || option.curly) + warning("Expected '{a}' and instead saw '{b}'.", + nexttoken, '{', nexttoken.value); + + noreach = true; + indent += option.indent; + // test indentation only if statement is in new line + a = [statement(nexttoken.line === token.line)]; + indent -= option.indent; + noreach = false; + } + funct['(verb)'] = null; + if (!ordinary || !option.funcscope) scope = s; + inblock = b; + if (ordinary && option.noempty && (!a || a.length === 0)) { + warning("Empty block."); + } + return a; + } + + + function countMember(m) { + if (membersOnly && typeof membersOnly[m] !== 'boolean') { + warning("Unexpected /*member '{a}'.", token, m); + } + if (typeof member[m] === 'number') { + member[m] += 1; + } else { + member[m] = 1; + } + } + + + function note_implied(token) { + var name = token.value, line = token.line, a = implied[name]; + if (typeof a === 'function') { + a = false; + } + + if (!a) { + a = [line]; + implied[name] = a; + } else if (a[a.length - 1] !== line) { + a.push(line); + } + } + + + // Build the syntax table by declaring the syntactic elements of the language. + + type('(number)', function () { + return this; + }); + + type('(string)', function () { + return this; + }); + + syntax['(identifier)'] = { + type: '(identifier)', + lbp: 0, + identifier: true, + nud: function () { + var v = this.value, + s = scope[v], + f; + + if (typeof s === 'function') { + // Protection against accidental inheritance. + s = undefined; + } else if (typeof s === 'boolean') { + f = funct; + funct = functions[0]; + addlabel(v, 'var'); + s = funct; + funct = f; + } + + // The name is in scope and defined in the current function. + if (funct === s) { + // Change 'unused' to 'var', and reject labels. + switch (funct[v]) { + case 'unused': + funct[v] = 'var'; + break; + case 'unction': + funct[v] = 'function'; + this['function'] = true; + break; + case 'function': + this['function'] = true; + break; + case 'label': + warning("'{a}' is a statement label.", token, v); + break; + } + } else if (funct['(global)']) { + // The name is not defined in the function. If we are in the global + // scope, then we have an undefined variable. + // + // Operators typeof and delete do not raise runtime errors even if + // the base object of a reference is null so no need to display warning + // if we're inside of typeof or delete. + + if (option.undef && typeof predefined[v] !== 'boolean') { + // Attempting to subscript a null reference will throw an + // error, even within the typeof and delete operators + if (!(anonname === 'typeof' || anonname === 'delete') || + (nexttoken && (nexttoken.value === '.' || nexttoken.value === '['))) { + + isundef(funct, "'{a}' is not defined.", token, v); + } + } + note_implied(token); + } else { + // If the name is already defined in the current + // function, but not as outer, then there is a scope error. + + switch (funct[v]) { + case 'closure': + case 'function': + case 'var': + case 'unused': + warning("'{a}' used out of scope.", token, v); + break; + case 'label': + warning("'{a}' is a statement label.", token, v); + break; + case 'outer': + case 'global': + break; + default: + // If the name is defined in an outer function, make an outer entry, + // and if it was unused, make it var. + if (s === true) { + funct[v] = true; + } else if (s === null) { + warning("'{a}' is not allowed.", token, v); + note_implied(token); + } else if (typeof s !== 'object') { + // Operators typeof and delete do not raise runtime errors even + // if the base object of a reference is null so no need to + // display warning if we're inside of typeof or delete. + if (option.undef) { + // Attempting to subscript a null reference will throw an + // error, even within the typeof and delete operators + if (!(anonname === 'typeof' || anonname === 'delete') || + (nexttoken && + (nexttoken.value === '.' || nexttoken.value === '['))) { + + isundef(funct, "'{a}' is not defined.", token, v); + } + } + funct[v] = true; + note_implied(token); + } else { + switch (s[v]) { + case 'function': + case 'unction': + this['function'] = true; + s[v] = 'closure'; + funct[v] = s['(global)'] ? 'global' : 'outer'; + break; + case 'var': + case 'unused': + s[v] = 'closure'; + funct[v] = s['(global)'] ? 'global' : 'outer'; + break; + case 'closure': + case 'parameter': + funct[v] = s['(global)'] ? 'global' : 'outer'; + break; + case 'label': + warning("'{a}' is a statement label.", token, v); + } + } + } + } + return this; + }, + led: function () { + error("Expected an operator and instead saw '{a}'.", + nexttoken, nexttoken.value); + } + }; + + type('(regexp)', function () { + return this; + }); + + +// ECMAScript parser + + delim('(endline)'); + delim('(begin)'); + delim('(end)').reach = true; + delim(''); + delim('(error)').reach = true; + delim('}').reach = true; + delim(')'); + delim(']'); + delim('"').reach = true; + delim("'").reach = true; + delim(';'); + delim(':').reach = true; + delim(','); + delim('#'); + delim('@'); + reserve('else'); + reserve('case').reach = true; + reserve('catch'); + reserve('default').reach = true; + reserve('finally'); + reservevar('arguments', function (x) { + if (directive['use strict'] && funct['(global)']) { + warning("Strict violation.", x); + } + }); + reservevar('eval'); + reservevar('false'); + reservevar('Infinity'); + reservevar('NaN'); + reservevar('null'); + reservevar('this', function (x) { + if (directive['use strict'] && !option.validthis && ((funct['(statement)'] && + funct['(name)'].charAt(0) > 'Z') || funct['(global)'])) { + warning("Possible strict violation.", x); + } + }); + reservevar('true'); + reservevar('undefined'); + assignop('=', 'assign', 20); + assignop('+=', 'assignadd', 20); + assignop('-=', 'assignsub', 20); + assignop('*=', 'assignmult', 20); + assignop('/=', 'assigndiv', 20).nud = function () { + error("A regular expression literal can be confused with '/='."); + }; + assignop('%=', 'assignmod', 20); + bitwiseassignop('&=', 'assignbitand', 20); + bitwiseassignop('|=', 'assignbitor', 20); + bitwiseassignop('^=', 'assignbitxor', 20); + bitwiseassignop('<<=', 'assignshiftleft', 20); + bitwiseassignop('>>=', 'assignshiftright', 20); + bitwiseassignop('>>>=', 'assignshiftrightunsigned', 20); + infix('?', function (left, that) { + that.left = left; + that.right = expression(10); + advance(':'); + that['else'] = expression(10); + return that; + }, 30); + + infix('||', 'or', 40); + infix('&&', 'and', 50); + bitwise('|', 'bitor', 70); + bitwise('^', 'bitxor', 80); + bitwise('&', 'bitand', 90); + relation('==', function (left, right) { + var eqnull = option.eqnull && (left.value === 'null' || right.value === 'null'); + + if (!eqnull && option.eqeqeq) + warning("Expected '{a}' and instead saw '{b}'.", this, '===', '=='); + else if (isPoorRelation(left)) + warning("Use '{a}' to compare with '{b}'.", this, '===', left.value); + else if (isPoorRelation(right)) + warning("Use '{a}' to compare with '{b}'.", this, '===', right.value); + + return this; + }); + relation('==='); + relation('!=', function (left, right) { + var eqnull = option.eqnull && + (left.value === 'null' || right.value === 'null'); + + if (!eqnull && option.eqeqeq) { + warning("Expected '{a}' and instead saw '{b}'.", + this, '!==', '!='); + } else if (isPoorRelation(left)) { + warning("Use '{a}' to compare with '{b}'.", + this, '!==', left.value); + } else if (isPoorRelation(right)) { + warning("Use '{a}' to compare with '{b}'.", + this, '!==', right.value); + } + return this; + }); + relation('!=='); + relation('<'); + relation('>'); + relation('<='); + relation('>='); + bitwise('<<', 'shiftleft', 120); + bitwise('>>', 'shiftright', 120); + bitwise('>>>', 'shiftrightunsigned', 120); + infix('in', 'in', 120); + infix('instanceof', 'instanceof', 120); + infix('+', function (left, that) { + var right = expression(130); + if (left && right && left.id === '(string)' && right.id === '(string)') { + left.value += right.value; + left.character = right.character; + if (!option.scripturl && jx.test(left.value)) { + warning("JavaScript URL.", left); + } + return left; + } + that.left = left; + that.right = right; + return that; + }, 130); + prefix('+', 'num'); + prefix('+++', function () { + warning("Confusing pluses."); + this.right = expression(150); + this.arity = 'unary'; + return this; + }); + infix('+++', function (left) { + warning("Confusing pluses."); + this.left = left; + this.right = expression(130); + return this; + }, 130); + infix('-', 'sub', 130); + prefix('-', 'neg'); + prefix('---', function () { + warning("Confusing minuses."); + this.right = expression(150); + this.arity = 'unary'; + return this; + }); + infix('---', function (left) { + warning("Confusing minuses."); + this.left = left; + this.right = expression(130); + return this; + }, 130); + infix('*', 'mult', 140); + infix('/', 'div', 140); + infix('%', 'mod', 140); + + suffix('++', 'postinc'); + prefix('++', 'preinc'); + syntax['++'].exps = true; + + suffix('--', 'postdec'); + prefix('--', 'predec'); + syntax['--'].exps = true; + prefix('delete', function () { + var p = expression(0); + if (!p || (p.id !== '.' && p.id !== '[')) { + warning("Variables should not be deleted."); + } + this.first = p; + return this; + }).exps = true; + + prefix('~', function () { + if (option.bitwise) { + warning("Unexpected '{a}'.", this, '~'); + } + expression(150); + return this; + }); + + prefix('!', function () { + this.right = expression(150); + this.arity = 'unary'; + if (bang[this.right.id] === true) { + warning("Confusing use of '{a}'.", this, '!'); + } + return this; + }); + prefix('typeof', 'typeof'); + prefix('new', function () { + var c = expression(155), i; + if (c && c.id !== 'function') { + if (c.identifier) { + c['new'] = true; + switch (c.value) { + case 'Object': + warning("Use the object literal notation {}.", token); + break; + case 'Number': + case 'String': + case 'Boolean': + case 'Math': + case 'JSON': + warning("Do not use {a} as a constructor.", token, c.value); + break; + case 'Function': + if (!option.evil) { + warning("The Function constructor is eval."); + } + break; + case 'Date': + case 'RegExp': + break; + default: + if (c.id !== 'function') { + i = c.value.substr(0, 1); + if (option.newcap && (i < 'A' || i > 'Z')) { + warning("A constructor name should start with an uppercase letter.", + token); + } + } + } + } else { + if (c.id !== '.' && c.id !== '[' && c.id !== '(') { + warning("Bad constructor.", token); + } + } + } else { + if (!option.supernew) + warning("Weird construction. Delete 'new'.", this); + } + adjacent(token, nexttoken); + if (nexttoken.id !== '(' && !option.supernew) { + warning("Missing '()' invoking a constructor."); + } + this.first = c; + return this; + }); + syntax['new'].exps = true; + + prefix('void').exps = true; + + infix('.', function (left, that) { + adjacent(prevtoken, token); + nobreak(); + var m = identifier(); + if (typeof m === 'string') { + countMember(m); + } + that.left = left; + that.right = m; + if (left && left.value === 'arguments' && (m === 'callee' || m === 'caller')) { + if (option.noarg) + warning("Avoid arguments.{a}.", left, m); + else if (directive['use strict']) + error('Strict violation.'); + } else if (!option.evil && left && left.value === 'document' && + (m === 'write' || m === 'writeln')) { + warning("document.write can be a form of eval.", left); + } + if (!option.evil && (m === 'eval' || m === 'execScript')) { + warning('eval is evil.'); + } + return that; + }, 160, true); + + infix('(', function (left, that) { + if (prevtoken.id !== '}' && prevtoken.id !== ')') { + nobreak(prevtoken, token); + } + nospace(); + if (option.immed && !left.immed && left.id === 'function') { + warning("Wrap an immediate function invocation in parentheses " + + "to assist the reader in understanding that the expression " + + "is the result of a function, and not the function itself."); + } + var n = 0, + p = []; + if (left) { + if (left.type === '(identifier)') { + if (left.value.match(/^[A-Z]([A-Z0-9_$]*[a-z][A-Za-z0-9_$]*)?$/)) { + if (left.value !== 'Number' && left.value !== 'String' && + left.value !== 'Boolean' && + left.value !== 'Date') { + if (left.value === 'Math') { + warning("Math is not a function.", left); + } else if (option.newcap) { + warning( +"Missing 'new' prefix when invoking a constructor.", left); + } + } + } + } + } + if (nexttoken.id !== ')') { + for (;;) { + p[p.length] = expression(10); + n += 1; + if (nexttoken.id !== ',') { + break; + } + comma(); + } + } + advance(')'); + nospace(prevtoken, token); + if (typeof left === 'object') { + if (left.value === 'parseInt' && n === 1) { + warning("Missing radix parameter.", left); + } + if (!option.evil) { + if (left.value === 'eval' || left.value === 'Function' || + left.value === 'execScript') { + warning("eval is evil.", left); + } else if (p[0] && p[0].id === '(string)' && + (left.value === 'setTimeout' || + left.value === 'setInterval')) { + warning( + "Implied eval is evil. Pass a function instead of a string.", left); + } + } + if (!left.identifier && left.id !== '.' && left.id !== '[' && + left.id !== '(' && left.id !== '&&' && left.id !== '||' && + left.id !== '?') { + warning("Bad invocation.", left); + } + } + that.left = left; + return that; + }, 155, true).exps = true; + + prefix('(', function () { + nospace(); + if (nexttoken.id === 'function') { + nexttoken.immed = true; + } + var v = expression(0); + advance(')', this); + nospace(prevtoken, token); + if (option.immed && v.id === 'function') { + if (nexttoken.id === '(' || + (nexttoken.id === '.' && (peek().value === 'call' || peek().value === 'apply'))) { + warning( +"Move the invocation into the parens that contain the function.", nexttoken); + } else { + warning( +"Do not wrap function literals in parens unless they are to be immediately invoked.", + this); + } + } + return v; + }); + + infix('[', function (left, that) { + nobreak(prevtoken, token); + nospace(); + var e = expression(0), s; + if (e && e.type === '(string)') { + if (!option.evil && (e.value === 'eval' || e.value === 'execScript')) { + warning("eval is evil.", that); + } + countMember(e.value); + if (!option.sub && ix.test(e.value)) { + s = syntax[e.value]; + if (!s || !s.reserved) { + warning("['{a}'] is better written in dot notation.", + e, e.value); + } + } + } + advance(']', that); + nospace(prevtoken, token); + that.left = left; + that.right = e; + return that; + }, 160, true); + + prefix('[', function () { + var b = token.line !== nexttoken.line; + this.first = []; + if (b) { + indent += option.indent; + if (nexttoken.from === indent + option.indent) { + indent += option.indent; + } + } + while (nexttoken.id !== '(end)') { + while (nexttoken.id === ',') { + warning("Extra comma."); + advance(','); + } + if (nexttoken.id === ']') { + break; + } + if (b && token.line !== nexttoken.line) { + indentation(); + } + this.first.push(expression(10)); + if (nexttoken.id === ',') { + comma(); + if (nexttoken.id === ']' && !option.es5) { + warning("Extra comma.", token); + break; + } + } else { + break; + } + } + if (b) { + indent -= option.indent; + indentation(); + } + advance(']', this); + return this; + }, 160); + + + function property_name() { + var id = optionalidentifier(true); + if (!id) { + if (nexttoken.id === '(string)') { + id = nexttoken.value; + advance(); + } else if (nexttoken.id === '(number)') { + id = nexttoken.value.toString(); + advance(); + } + } + return id; + } + + + function functionparams() { + var i, t = nexttoken, p = []; + advance('('); + nospace(); + if (nexttoken.id === ')') { + advance(')'); + return; + } + for (;;) { + i = identifier(true); + p.push(i); + addlabel(i, 'parameter'); + if (nexttoken.id === ',') { + comma(); + } else { + advance(')', t); + nospace(prevtoken, token); + return p; + } + } + } + + + function doFunction(i, statement) { + var f, + oldOption = option, + oldScope = scope; + + option = Object.create(option); + scope = Object.create(scope); + + funct = { + '(name)' : i || '"' + anonname + '"', + '(line)' : nexttoken.line, + '(context)' : funct, + '(breakage)' : 0, + '(loopage)' : 0, + '(scope)' : scope, + '(statement)': statement + }; + f = funct; + token.funct = funct; + functions.push(funct); + if (i) { + addlabel(i, 'function'); + } + funct['(params)'] = functionparams(); + + block(false, false, true); + scope = oldScope; + option = oldOption; + funct['(last)'] = token.line; + funct = funct['(context)']; + return f; + } + + + (function (x) { + x.nud = function () { + var b, f, i, j, p, t; + var props = {}; // All properties, including accessors + + function saveProperty(name, token) { + if (props[name] && is_own(props, name)) + warning("Duplicate member '{a}'.", nexttoken, i); + else + props[name] = {}; + + props[name].basic = true; + props[name].basicToken = token; + } + + function saveSetter(name, token) { + if (props[name] && is_own(props, name)) { + if (props[name].basic || props[name].setter) + warning("Duplicate member '{a}'.", nexttoken, i); + } else { + props[name] = {}; + } + + props[name].setter = true; + props[name].setterToken = token; + } + + function saveGetter(name) { + if (props[name] && is_own(props, name)) { + if (props[name].basic || props[name].getter) + warning("Duplicate member '{a}'.", nexttoken, i); + } else { + props[name] = {}; + } + + props[name].getter = true; + props[name].getterToken = token; + } + + b = token.line !== nexttoken.line; + if (b) { + indent += option.indent; + if (nexttoken.from === indent + option.indent) { + indent += option.indent; + } + } + for (;;) { + if (nexttoken.id === '}') { + break; + } + if (b) { + indentation(); + } + if (nexttoken.value === 'get' && peek().id !== ':') { + advance('get'); + if (!option.es5) { + error("get/set are ES5 features."); + } + i = property_name(); + if (!i) { + error("Missing property name."); + } + saveGetter(i); + t = nexttoken; + adjacent(token, nexttoken); + f = doFunction(); + p = f['(params)']; + if (p) { + warning("Unexpected parameter '{a}' in get {b} function.", t, p[0], i); + } + adjacent(token, nexttoken); + } else if (nexttoken.value === 'set' && peek().id !== ':') { + advance('set'); + if (!option.es5) { + error("get/set are ES5 features."); + } + i = property_name(); + if (!i) { + error("Missing property name."); + } + saveSetter(i, nexttoken); + t = nexttoken; + adjacent(token, nexttoken); + f = doFunction(); + p = f['(params)']; + if (!p || p.length !== 1) { + warning("Expected a single parameter in set {a} function.", t, i); + } + } else { + i = property_name(); + saveProperty(i, nexttoken); + if (typeof i !== 'string') { + break; + } + advance(':'); + nonadjacent(token, nexttoken); + expression(10); + } + + countMember(i); + if (nexttoken.id === ',') { + comma(); + if (nexttoken.id === ',') { + warning("Extra comma.", token); + } else if (nexttoken.id === '}' && !option.es5) { + warning("Extra comma.", token); + } + } else { + break; + } + } + if (b) { + indent -= option.indent; + indentation(); + } + advance('}', this); + + // Check for lonely setters if in the ES5 mode. + if (option.es5) { + for (var name in props) { + if (is_own(props, name) && props[name].setter && !props[name].getter) { + warning("Setter is defined without getter.", props[name].setterToken); + } + } + } + return this; + }; + x.fud = function () { + error("Expected to see a statement and instead saw a block.", token); + }; + }(delim('{'))); + +// This Function is called when esnext option is set to true +// it adds the `const` statement to JSHINT + + useESNextSyntax = function () { + var conststatement = stmt('const', function (prefix) { + var id, name, value; + + this.first = []; + for (;;) { + nonadjacent(token, nexttoken); + id = identifier(); + if (funct[id] === "const") { + warning("const '" + id + "' has already been declared"); + } + if (funct['(global)'] && predefined[id] === false) { + warning("Redefinition of '{a}'.", token, id); + } + addlabel(id, 'const'); + if (prefix) { + break; + } + name = token; + this.first.push(token); + + if (nexttoken.id !== "=") { + warning("const " + + "'{a}' is initialized to 'undefined'.", token, id); + } + + if (nexttoken.id === '=') { + nonadjacent(token, nexttoken); + advance('='); + nonadjacent(token, nexttoken); + if (nexttoken.id === 'undefined') { + warning("It is not necessary to initialize " + + "'{a}' to 'undefined'.", token, id); + } + if (peek(0).id === '=' && nexttoken.identifier) { + error("Constant {a} was not declared correctly.", + nexttoken, nexttoken.value); + } + value = expression(0); + name.first = value; + } + + if (nexttoken.id !== ',') { + break; + } + comma(); + } + return this; + }); + conststatement.exps = true; + }; + + var varstatement = stmt('var', function (prefix) { + // JavaScript does not have block scope. It only has function scope. So, + // declaring a variable in a block can have unexpected consequences. + var id, name, value; + + if (funct['(onevar)'] && option.onevar) { + warning("Too many var statements."); + } else if (!funct['(global)']) { + funct['(onevar)'] = true; + } + this.first = []; + for (;;) { + nonadjacent(token, nexttoken); + id = identifier(); + if (option.esnext && funct[id] === "const") { + warning("const '" + id + "' has already been declared"); + } + if (funct['(global)'] && predefined[id] === false) { + warning("Redefinition of '{a}'.", token, id); + } + addlabel(id, 'unused'); + if (prefix) { + break; + } + name = token; + this.first.push(token); + if (nexttoken.id === '=') { + nonadjacent(token, nexttoken); + advance('='); + nonadjacent(token, nexttoken); + if (nexttoken.id === 'undefined') { + warning("It is not necessary to initialize '{a}' to 'undefined'.", token, id); + } + if (peek(0).id === '=' && nexttoken.identifier) { + error("Variable {a} was not declared correctly.", + nexttoken, nexttoken.value); + } + value = expression(0); + name.first = value; + } + if (nexttoken.id !== ',') { + break; + } + comma(); + } + return this; + }); + varstatement.exps = true; + + blockstmt('function', function () { + if (inblock) { + warning("Function declarations should not be placed in blocks. " + + "Use a function expression or move the statement to the top of " + + "the outer function.", token); + + } + var i = identifier(); + if (option.esnext && funct[i] === "const") { + warning("const '" + i + "' has already been declared"); + } + adjacent(token, nexttoken); + addlabel(i, 'unction'); + doFunction(i, true); + if (nexttoken.id === '(' && nexttoken.line === token.line) { + error( +"Function declarations are not invocable. Wrap the whole function invocation in parens."); + } + return this; + }); + + prefix('function', function () { + var i = optionalidentifier(); + if (i) { + adjacent(token, nexttoken); + } else { + nonadjacent(token, nexttoken); + } + doFunction(i); + if (!option.loopfunc && funct['(loopage)']) { + warning("Don't make functions within a loop."); + } + return this; + }); + + blockstmt('if', function () { + var t = nexttoken; + advance('('); + nonadjacent(this, t); + nospace(); + expression(20); + if (nexttoken.id === '=') { + if (!option.boss) + warning("Expected a conditional expression and instead saw an assignment."); + advance('='); + expression(20); + } + advance(')', t); + nospace(prevtoken, token); + block(true, true); + if (nexttoken.id === 'else') { + nonadjacent(token, nexttoken); + advance('else'); + if (nexttoken.id === 'if' || nexttoken.id === 'switch') { + statement(true); + } else { + block(true, true); + } + } + return this; + }); + + blockstmt('try', function () { + var b, e, s; + + block(false); + if (nexttoken.id === 'catch') { + advance('catch'); + nonadjacent(token, nexttoken); + advance('('); + s = scope; + scope = Object.create(s); + e = nexttoken.value; + if (nexttoken.type !== '(identifier)') { + warning("Expected an identifier and instead saw '{a}'.", + nexttoken, e); + } else { + addlabel(e, 'exception'); + } + advance(); + advance(')'); + block(false); + b = true; + scope = s; + } + if (nexttoken.id === 'finally') { + advance('finally'); + block(false); + return; + } else if (!b) { + error("Expected '{a}' and instead saw '{b}'.", + nexttoken, 'catch', nexttoken.value); + } + return this; + }); + + blockstmt('while', function () { + var t = nexttoken; + funct['(breakage)'] += 1; + funct['(loopage)'] += 1; + advance('('); + nonadjacent(this, t); + nospace(); + expression(20); + if (nexttoken.id === '=') { + if (!option.boss) + warning("Expected a conditional expression and instead saw an assignment."); + advance('='); + expression(20); + } + advance(')', t); + nospace(prevtoken, token); + block(true, true); + funct['(breakage)'] -= 1; + funct['(loopage)'] -= 1; + return this; + }).labelled = true; + + reserve('with'); + + blockstmt('switch', function () { + var t = nexttoken, + g = false; + funct['(breakage)'] += 1; + advance('('); + nonadjacent(this, t); + nospace(); + this.condition = expression(20); + advance(')', t); + nospace(prevtoken, token); + nonadjacent(token, nexttoken); + t = nexttoken; + advance('{'); + nonadjacent(token, nexttoken); + indent += option.indent; + this.cases = []; + for (;;) { + switch (nexttoken.id) { + case 'case': + switch (funct['(verb)']) { + case 'break': + case 'case': + case 'continue': + case 'return': + case 'switch': + case 'throw': + break; + default: + // You can tell JSHint that you don't use break intentionally by + // adding a comment /* falls through */ on a line just before + // the next `case`. + if (!ft.test(lines[nexttoken.line - 2])) { + warning( + "Expected a 'break' statement before 'case'.", + token); + } + } + indentation(-option.indent); + advance('case'); + this.cases.push(expression(20)); + g = true; + advance(':'); + funct['(verb)'] = 'case'; + break; + case 'default': + switch (funct['(verb)']) { + case 'break': + case 'continue': + case 'return': + case 'throw': + break; + default: + if (!ft.test(lines[nexttoken.line - 2])) { + warning( + "Expected a 'break' statement before 'default'.", + token); + } + } + indentation(-option.indent); + advance('default'); + g = true; + advance(':'); + break; + case '}': + indent -= option.indent; + indentation(); + advance('}', t); + if (this.cases.length === 1 || this.condition.id === 'true' || + this.condition.id === 'false') { + if (!option.onecase) + warning("This 'switch' should be an 'if'.", this); + } + funct['(breakage)'] -= 1; + funct['(verb)'] = undefined; + return; + case '(end)': + error("Missing '{a}'.", nexttoken, '}'); + return; + default: + if (g) { + switch (token.id) { + case ',': + error("Each value should have its own case label."); + return; + case ':': + g = false; + statements(); + break; + default: + error("Missing ':' on a case clause.", token); + return; + } + } else { + if (token.id === ':') { + advance(':'); + error("Unexpected '{a}'.", token, ':'); + statements(); + } else { + error("Expected '{a}' and instead saw '{b}'.", + nexttoken, 'case', nexttoken.value); + return; + } + } + } + } + }).labelled = true; + + stmt('debugger', function () { + if (!option.debug) { + warning("All 'debugger' statements should be removed."); + } + return this; + }).exps = true; + + (function () { + var x = stmt('do', function () { + funct['(breakage)'] += 1; + funct['(loopage)'] += 1; + this.first = block(true); + advance('while'); + var t = nexttoken; + nonadjacent(token, t); + advance('('); + nospace(); + expression(20); + if (nexttoken.id === '=') { + if (!option.boss) + warning("Expected a conditional expression and instead saw an assignment."); + advance('='); + expression(20); + } + advance(')', t); + nospace(prevtoken, token); + funct['(breakage)'] -= 1; + funct['(loopage)'] -= 1; + return this; + }); + x.labelled = true; + x.exps = true; + }()); + + blockstmt('for', function () { + var s, t = nexttoken; + funct['(breakage)'] += 1; + funct['(loopage)'] += 1; + advance('('); + nonadjacent(this, t); + nospace(); + if (peek(nexttoken.id === 'var' ? 1 : 0).id === 'in') { + if (nexttoken.id === 'var') { + advance('var'); + varstatement.fud.call(varstatement, true); + } else { + switch (funct[nexttoken.value]) { + case 'unused': + funct[nexttoken.value] = 'var'; + break; + case 'var': + break; + default: + warning("Bad for in variable '{a}'.", + nexttoken, nexttoken.value); + } + advance(); + } + advance('in'); + expression(20); + advance(')', t); + s = block(true, true); + if (option.forin && s && (s.length > 1 || typeof s[0] !== 'object' || + s[0].value !== 'if')) { + warning("The body of a for in should be wrapped in an if statement to filter " + + "unwanted properties from the prototype.", this); + } + funct['(breakage)'] -= 1; + funct['(loopage)'] -= 1; + return this; + } else { + if (nexttoken.id !== ';') { + if (nexttoken.id === 'var') { + advance('var'); + varstatement.fud.call(varstatement); + } else { + for (;;) { + expression(0, 'for'); + if (nexttoken.id !== ',') { + break; + } + comma(); + } + } + } + nolinebreak(token); + advance(';'); + if (nexttoken.id !== ';') { + expression(20); + if (nexttoken.id === '=') { + if (!option.boss) + warning("Expected a conditional expression and instead saw an assignment."); + advance('='); + expression(20); + } + } + nolinebreak(token); + advance(';'); + if (nexttoken.id === ';') { + error("Expected '{a}' and instead saw '{b}'.", + nexttoken, ')', ';'); + } + if (nexttoken.id !== ')') { + for (;;) { + expression(0, 'for'); + if (nexttoken.id !== ',') { + break; + } + comma(); + } + } + advance(')', t); + nospace(prevtoken, token); + block(true, true); + funct['(breakage)'] -= 1; + funct['(loopage)'] -= 1; + return this; + } + }).labelled = true; + + + stmt('break', function () { + var v = nexttoken.value; + + if (funct['(breakage)'] === 0) + warning("Unexpected '{a}'.", nexttoken, this.value); + + if (!option.asi) + nolinebreak(this); + + if (nexttoken.id !== ';') { + if (token.line === nexttoken.line) { + if (funct[v] !== 'label') { + warning("'{a}' is not a statement label.", nexttoken, v); + } else if (scope[v] !== funct) { + warning("'{a}' is out of scope.", nexttoken, v); + } + this.first = nexttoken; + advance(); + } + } + reachable('break'); + return this; + }).exps = true; + + + stmt('continue', function () { + var v = nexttoken.value; + + if (funct['(breakage)'] === 0) + warning("Unexpected '{a}'.", nexttoken, this.value); + + if (!option.asi) + nolinebreak(this); + + if (nexttoken.id !== ';') { + if (token.line === nexttoken.line) { + if (funct[v] !== 'label') { + warning("'{a}' is not a statement label.", nexttoken, v); + } else if (scope[v] !== funct) { + warning("'{a}' is out of scope.", nexttoken, v); + } + this.first = nexttoken; + advance(); + } + } else if (!funct['(loopage)']) { + warning("Unexpected '{a}'.", nexttoken, this.value); + } + reachable('continue'); + return this; + }).exps = true; + + + stmt('return', function () { + if (this.line === nexttoken.line) { + if (nexttoken.id === '(regexp)') + warning("Wrap the /regexp/ literal in parens to disambiguate the slash operator."); + + if (nexttoken.id !== ';' && !nexttoken.reach) { + nonadjacent(token, nexttoken); + if (peek().value === "=" && !option.boss) { + warningAt("Did you mean to return a conditional instead of an assignment?", + token.line, token.character + 1); + } + this.first = expression(0); + } + } else if (!option.asi) { + nolinebreak(this); // always warn (Line breaking error) + } + reachable('return'); + return this; + }).exps = true; + + + stmt('throw', function () { + nolinebreak(this); + nonadjacent(token, nexttoken); + this.first = expression(20); + reachable('throw'); + return this; + }).exps = true; + +// Superfluous reserved words + + reserve('class'); + reserve('const'); + reserve('enum'); + reserve('export'); + reserve('extends'); + reserve('import'); + reserve('super'); + + reserve('let'); + reserve('yield'); + reserve('implements'); + reserve('interface'); + reserve('package'); + reserve('private'); + reserve('protected'); + reserve('public'); + reserve('static'); + + +// Parse JSON + + function jsonValue() { + + function jsonObject() { + var o = {}, t = nexttoken; + advance('{'); + if (nexttoken.id !== '}') { + for (;;) { + if (nexttoken.id === '(end)') { + error("Missing '}' to match '{' from line {a}.", + nexttoken, t.line); + } else if (nexttoken.id === '}') { + warning("Unexpected comma.", token); + break; + } else if (nexttoken.id === ',') { + error("Unexpected comma.", nexttoken); + } else if (nexttoken.id !== '(string)') { + warning("Expected a string and instead saw {a}.", + nexttoken, nexttoken.value); + } + if (o[nexttoken.value] === true) { + warning("Duplicate key '{a}'.", + nexttoken, nexttoken.value); + } else if ((nexttoken.value === '__proto__' && + !option.proto) || (nexttoken.value === '__iterator__' && + !option.iterator)) { + warning("The '{a}' key may produce unexpected results.", + nexttoken, nexttoken.value); + } else { + o[nexttoken.value] = true; + } + advance(); + advance(':'); + jsonValue(); + if (nexttoken.id !== ',') { + break; + } + advance(','); + } + } + advance('}'); + } + + function jsonArray() { + var t = nexttoken; + advance('['); + if (nexttoken.id !== ']') { + for (;;) { + if (nexttoken.id === '(end)') { + error("Missing ']' to match '[' from line {a}.", + nexttoken, t.line); + } else if (nexttoken.id === ']') { + warning("Unexpected comma.", token); + break; + } else if (nexttoken.id === ',') { + error("Unexpected comma.", nexttoken); + } + jsonValue(); + if (nexttoken.id !== ',') { + break; + } + advance(','); + } + } + advance(']'); + } + + switch (nexttoken.id) { + case '{': + jsonObject(); + break; + case '[': + jsonArray(); + break; + case 'true': + case 'false': + case 'null': + case '(number)': + case '(string)': + advance(); + break; + case '-': + advance('-'); + if (token.character !== nexttoken.from) { + warning("Unexpected space after '-'.", token); + } + adjacent(token, nexttoken); + advance('(number)'); + break; + default: + error("Expected a JSON value.", nexttoken); + } + } + + +// The actual JSHINT function itself. + + var itself = function (s, o, g) { + var a, i, k; + JSHINT.errors = []; + JSHINT.undefs = []; + predefined = Object.create(standard); + combine(predefined, g || {}); + if (o) { + a = o.predef; + if (a) { + if (Array.isArray(a)) { + for (i = 0; i < a.length; i += 1) { + predefined[a[i]] = true; + } + } else if (typeof a === 'object') { + k = Object.keys(a); + for (i = 0; i < k.length; i += 1) { + predefined[k[i]] = !!a[k[i]]; + } + } + } + option = o; + } else { + option = {}; + } + option.indent = option.indent || 4; + option.maxerr = option.maxerr || 50; + + tab = ''; + for (i = 0; i < option.indent; i += 1) { + tab += ' '; + } + indent = 1; + global = Object.create(predefined); + scope = global; + funct = { + '(global)': true, + '(name)': '(global)', + '(scope)': scope, + '(breakage)': 0, + '(loopage)': 0 + }; + functions = [funct]; + urls = []; + stack = null; + member = {}; + membersOnly = null; + implied = {}; + inblock = false; + lookahead = []; + jsonmode = false; + warnings = 0; + lex.init(s); + prereg = true; + directive = {}; + + prevtoken = token = nexttoken = syntax['(begin)']; + assume(); + + // combine the passed globals after we've assumed all our options + combine(predefined, g || {}); + + //reset values + comma.first = true; + + try { + advance(); + switch (nexttoken.id) { + case '{': + case '[': + option.laxbreak = true; + jsonmode = true; + jsonValue(); + break; + default: + directives(); + if (directive["use strict"] && !option.globalstrict) { + warning("Use the function form of \"use strict\".", prevtoken); + } + + statements(); + } + advance('(end)'); + + var markDefined = function (name, context) { + do { + if (typeof context[name] === 'string') { + // JSHINT marks unused variables as 'unused' and + // unused function declaration as 'unction'. This + // code changes such instances back 'var' and + // 'closure' so that the code in JSHINT.data() + // doesn't think they're unused. + + if (context[name] === 'unused') + context[name] = 'var'; + else if (context[name] === 'unction') + context[name] = 'closure'; + + return true; + } + + context = context['(context)']; + } while (context); + + return false; + }; + + var clearImplied = function (name, line) { + if (!implied[name]) + return; + + var newImplied = []; + for (var i = 0; i < implied[name].length; i += 1) { + if (implied[name][i] !== line) + newImplied.push(implied[name][i]); + } + + if (newImplied.length === 0) + delete implied[name]; + else + implied[name] = newImplied; + }; + + // Check queued 'x is not defined' instances to see if they're still undefined. + for (i = 0; i < JSHINT.undefs.length; i += 1) { + k = JSHINT.undefs[i].slice(0); + + if (markDefined(k[2].value, k[0])) { + clearImplied(k[2].value, k[2].line); + } else { + warning.apply(warning, k.slice(1)); + } + } + } catch (e) { + if (e) { + var nt = nexttoken || {}; + JSHINT.errors.push({ + raw : e.raw, + reason : e.message, + line : e.line || nt.line, + character : e.character || nt.from + }, null); + } + } + + return JSHINT.errors.length === 0; + }; + + // Data summary. + itself.data = function () { + + var data = { functions: [], options: option }, fu, globals, implieds = [], f, i, j, + members = [], n, unused = [], v; + if (itself.errors.length) { + data.errors = itself.errors; + } + + if (jsonmode) { + data.json = true; + } + + for (n in implied) { + if (is_own(implied, n)) { + implieds.push({ + name: n, + line: implied[n] + }); + } + } + if (implieds.length > 0) { + data.implieds = implieds; + } + + if (urls.length > 0) { + data.urls = urls; + } + + globals = Object.keys(scope); + if (globals.length > 0) { + data.globals = globals; + } + for (i = 1; i < functions.length; i += 1) { + f = functions[i]; + fu = {}; + for (j = 0; j < functionicity.length; j += 1) { + fu[functionicity[j]] = []; + } + for (n in f) { + if (is_own(f, n) && n.charAt(0) !== '(') { + v = f[n]; + if (v === 'unction') { + v = 'unused'; + } + if (Array.isArray(fu[v])) { + fu[v].push(n); + if (v === 'unused') { + unused.push({ + name: n, + line: f['(line)'], + 'function': f['(name)'] + }); + } + } + } + } + for (j = 0; j < functionicity.length; j += 1) { + if (fu[functionicity[j]].length === 0) { + delete fu[functionicity[j]]; + } + } + fu.name = f['(name)']; + fu.param = f['(params)']; + fu.line = f['(line)']; + fu.last = f['(last)']; + data.functions.push(fu); + } + + if (unused.length > 0) { + data.unused = unused; + } + + members = []; + for (n in member) { + if (typeof member[n] === 'number') { + data.member = member; + break; + } + } + + return data; + }; + + itself.report = function (option) { + var data = itself.data(); + + var a = [], c, e, err, f, i, k, l, m = '', n, o = [], s; + + function detail(h, array) { + var b, i, singularity; + if (array) { + o.push('
' + h + ' '); + array = array.sort(); + for (i = 0; i < array.length; i += 1) { + if (array[i] !== singularity) { + singularity = array[i]; + o.push((b ? ', ' : '') + singularity); + b = true; + } + } + o.push('
'); + } + } + + + if (data.errors || data.implieds || data.unused) { + err = true; + o.push('
Error:'); + if (data.errors) { + for (i = 0; i < data.errors.length; i += 1) { + c = data.errors[i]; + if (c) { + e = c.evidence || ''; + o.push('

Problem' + (isFinite(c.line) ? ' at line ' + + c.line + ' character ' + c.character : '') + + ': ' + c.reason.entityify() + + '

' + + (e && (e.length > 80 ? e.slice(0, 77) + '...' : + e).entityify()) + '

'); + } + } + } + + if (data.implieds) { + s = []; + for (i = 0; i < data.implieds.length; i += 1) { + s[i] = '' + data.implieds[i].name + ' ' + + data.implieds[i].line + ''; + } + o.push('

Implied global: ' + s.join(', ') + '

'); + } + + if (data.unused) { + s = []; + for (i = 0; i < data.unused.length; i += 1) { + s[i] = '' + data.unused[i].name + ' ' + + data.unused[i].line + ' ' + + data.unused[i]['function'] + ''; + } + o.push('

Unused variable: ' + s.join(', ') + '

'); + } + if (data.json) { + o.push('

JSON: bad.

'); + } + o.push('
'); + } + + if (!option) { + + o.push('
'); + + if (data.urls) { + detail("URLs
", data.urls, '
'); + } + + if (data.json && !err) { + o.push('

JSON: good.

'); + } else if (data.globals) { + o.push('
Global ' + + data.globals.sort().join(', ') + '
'); + } else { + o.push('
No new global variables introduced.
'); + } + + for (i = 0; i < data.functions.length; i += 1) { + f = data.functions[i]; + + o.push('
' + f.line + '-' + + f.last + ' ' + (f.name || '') + '(' + + (f.param ? f.param.join(', ') : '') + ')
'); + detail('Unused', f.unused); + detail('Closure', f.closure); + detail('Variable', f['var']); + detail('Exception', f.exception); + detail('Outer', f.outer); + detail('Global', f.global); + detail('Label', f.label); + } + + if (data.member) { + a = Object.keys(data.member); + if (a.length) { + a = a.sort(); + m = '
/*members ';
+                    l = 10;
+                    for (i = 0; i < a.length; i += 1) {
+                        k = a[i];
+                        n = k.name();
+                        if (l + n.length > 72) {
+                            o.push(m + '
'); + m = ' '; + l = 1; + } + l += n.length + 2; + if (data.member[k] === 1) { + n = '' + n + ''; + } + if (i < a.length - 1) { + n += ', '; + } + m += n; + } + o.push(m + '
*/
'); + } + o.push('
'); + } + } + return o.join(''); + }; + + itself.jshint = itself; + + return itself; +}()); + +// Make JSHINT a Node module, if possible. +if (typeof exports === 'object' && exports) + exports.JSHINT = JSHINT; diff --git a/sublimelinter/modules/libs/jshint/jshint_jsc.js b/sublimelinter/modules/libs/jshint/jshint_jsc.js new file mode 100644 index 00000000..3918a6b7 --- /dev/null +++ b/sublimelinter/modules/libs/jshint/jshint_jsc.js @@ -0,0 +1,65 @@ +/*jshint boss: true, evil: true */ +/*globals load quit readline JSHINT */ + +// usage: +// jsc ${envHome}/jsc.js -- ${lineCount} {option1:true,option2:false} ${envHome} +var envHome = ''; + +if (arguments.length > 2) { + envHome = arguments[2].toString().replace(/\/env$/, '/'); +} + +load(envHome + "jshint.js"); + +if (typeof(JSHINT) === 'undefined') { + print('jshint: Could not load jshint.js, tried "' + envHome + 'jshint.js".'); + quit(); +} + +var process = function (args) { + var lineCount = parseInt(args[0], 10), + opts = (function (arg) { + switch (arg) { + case undefined: + case '': + return {}; + default: + return eval('(' + arg + ')'); + } + })(args[1]); + + if (isNaN(lineCount)) { + print('jshint: Must provide number of lines to read from stdin.'); + quit(); + } + + var input = readline(); + + for (var i = 0; i < lineCount; ++i) { + input += '\n' + readline(); + } + + var results = [], + err; + + try + { + if (!JSHINT(input, opts)) { + for (i = 0; err = JSHINT.errors[i]; i++) { + results.push(err); + } + } + } + catch (e) { + results.push({line: 1, character: 1, reason: e.message}); + + for (i = 0; err = JSHINT.errors[i]; i++) { + results.push(err); + } + } + + print(JSON.stringify(results)); + quit(); +}; + +process(arguments); diff --git a/sublimelinter/modules/libs/jshint/jshint_node.js b/sublimelinter/modules/libs/jshint/jshint_node.js new file mode 100644 index 00000000..d44db6ad --- /dev/null +++ b/sublimelinter/modules/libs/jshint/jshint_node.js @@ -0,0 +1,74 @@ +/*jshint node:true */ + +/* + Created by Aparajita Fishman (https://github.com/aparajita) + + This code is adapted from the node.js jshint module to work with stdin instead of a file, + and to take jshint options from the command line rather than a file. + + ** Licensed Under ** + + The MIT License + http://www.opensource.org/licenses/mit-license.php + + usage: node /path/to/jshint_node.js ["{option1:true,option2:false}"] + */ + +var _fs = require('fs'), + _util = require('util'), + _path = require('path'), + _jshint = require(_path.join(_path.dirname(process.argv[1]), 'jshint.js')); + +function hint(code, config) +{ + var results = []; + + try { + if (!_jshint.JSHINT(code, config)) { + _jshint.JSHINT.errors.forEach(function (error) { + if (error) { + results.push(error); + } + }); + } + } + catch (e) { + results.push({line: 1, character: 1, reason: e.message}); + + _jshint.JSHINT.errors.forEach(function (error) { + if (error) { + results.push(error); + } + }); + } + + _util.puts(JSON.stringify(results)); + process.exit(0); +} + +function run() +{ + var code = '', + config = JSON.parse(process.argv[2] || '{}'), + filename = process.argv[3] || ''; + + if (filename) + { + hint(_fs.readFileSync(filename, 'utf-8'), config); + } + else + { + process.stdin.resume(); + process.stdin.setEncoding('utf8'); + + process.stdin.on('data', function (chunk) { + code += chunk; + }); + + process.stdin.on('end', function () { + hint(code, config); + }); + } +} + +run(); diff --git a/sublimelinter/modules/libs/pep8.py b/sublimelinter/modules/libs/pep8.py new file mode 100644 index 00000000..227a9a3a --- /dev/null +++ b/sublimelinter/modules/libs/pep8.py @@ -0,0 +1,1360 @@ +#!/usr/bin/python +# pep8.py - Check Python source code formatting, according to PEP 8 +# Copyright (C) 2006 Johann C. Rocholl +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +Check Python source code formatting, according to PEP 8: +http://www.python.org/dev/peps/pep-0008/ + +For usage and a list of options, try this: +$ python pep8.py -h + +This program and its regression test suite live here: +http://github.com/jcrocholl/pep8 + +Groups of errors and warnings: +E errors +W warnings +100 indentation +200 whitespace +300 blank lines +400 imports +500 line length +600 deprecation +700 statements + +You can add checks to this program by writing plugins. Each plugin is +a simple function that is called for each line of source code, either +physical or logical. + +Physical line: +- Raw line of text from the input file. + +Logical line: +- Multi-line statements converted to a single line. +- Stripped left and right. +- Contents of strings replaced with 'xxx' of same length. +- Comments removed. + +The check function requests physical or logical lines by the name of +the first argument: + +def maximum_line_length(physical_line) +def extraneous_whitespace(logical_line) +def blank_lines(logical_line, blank_lines, indent_level, line_number) + +The last example above demonstrates how check plugins can request +additional information with extra arguments. All attributes of the +Checker object are available. Some examples: + +lines: a list of the raw lines from the input file +tokens: the tokens that contribute to this logical line +line_number: line number in the input file +blank_lines: blank lines before this one +indent_char: first indentation character in this file (' ' or '\t') +indent_level: indentation (with tabs expanded to multiples of 8) +previous_indent_level: indentation on previous line +previous_logical: previous logical line + +The docstring of each check function shall be the relevant part of +text from PEP 8. It is printed if the user enables --show-pep8. +Several docstrings contain examples directly from the PEP 8 document. + +Okay: spam(ham[1], {eggs: 2}) +E201: spam( ham[1], {eggs: 2}) + +These examples are verified automatically when pep8.py is run with the +--doctest option. You can add examples for your own check functions. +The format is simple: "Okay" or error/warning code followed by colon +and space, the rest of the line is example source code. If you put 'r' +before the docstring, you can use \n for newline, \t for tab and \s +for space. + +""" + +__version__ = '0.5.1dev' + +import os +import sys +import re +import time +import inspect +import keyword +import tokenize +from optparse import OptionParser +from fnmatch import fnmatch +try: + frozenset +except NameError: + from sets import ImmutableSet as frozenset + + +DEFAULT_EXCLUDE = '.svn,CVS,.bzr,.hg,.git' +DEFAULT_IGNORE = 'E24' +MAX_LINE_LENGTH = 79 + +INDENT_REGEX = re.compile(r'([ \t]*)') +RAISE_COMMA_REGEX = re.compile(r'raise\s+\w+\s*(,)') +SELFTEST_REGEX = re.compile(r'(Okay|[EW]\d{3}):\s(.*)') +ERRORCODE_REGEX = re.compile(r'[EW]\d{3}') +DOCSTRING_REGEX = re.compile(r'u?r?["\']') +WHITESPACE_AROUND_OPERATOR_REGEX = \ + re.compile('([^\w\s]*)\s*(\t| )\s*([^\w\s]*)') +EXTRANEOUS_WHITESPACE_REGEX = re.compile(r'[[({] | []}),;:]') +WHITESPACE_AROUND_NAMED_PARAMETER_REGEX = \ + re.compile(r'[()]|\s=[^=]|[^=!<>]=\s') + + +WHITESPACE = ' \t' + +BINARY_OPERATORS = frozenset(['**=', '*=', '+=', '-=', '!=', '<>', + '%=', '^=', '&=', '|=', '==', '/=', '//=', '<=', '>=', '<<=', '>>=', + '%', '^', '&', '|', '=', '/', '//', '<', '>', '<<']) +UNARY_OPERATORS = frozenset(['>>', '**', '*', '+', '-']) +OPERATORS = BINARY_OPERATORS | UNARY_OPERATORS +SKIP_TOKENS = frozenset([tokenize.COMMENT, tokenize.NL, tokenize.INDENT, + tokenize.DEDENT, tokenize.NEWLINE]) +E225NOT_KEYWORDS = (frozenset(keyword.kwlist + ['print']) - + frozenset(['False', 'None', 'True'])) +BENCHMARK_KEYS = ('directories', 'files', 'logical lines', 'physical lines') + +options = None +args = None + + +############################################################################## +# Plugins (check functions) for physical lines +############################################################################## + + +def tabs_or_spaces(physical_line, indent_char): + r""" + Never mix tabs and spaces. + + The most popular way of indenting Python is with spaces only. The + second-most popular way is with tabs only. Code indented with a mixture + of tabs and spaces should be converted to using spaces exclusively. When + invoking the Python command line interpreter with the -t option, it issues + warnings about code that illegally mixes tabs and spaces. When using -tt + these warnings become errors. These options are highly recommended! + + Okay: if a == 0:\n a = 1\n b = 1 + E101: if a == 0:\n a = 1\n\tb = 1 + """ + indent = INDENT_REGEX.match(physical_line).group(1) + for offset, char in enumerate(indent): + if char != indent_char: + return offset, "E101 indentation contains mixed spaces and tabs" + + +def tabs_obsolete(physical_line): + r""" + For new projects, spaces-only are strongly recommended over tabs. Most + editors have features that make this easy to do. + + Okay: if True:\n return + W191: if True:\n\treturn + """ + indent = INDENT_REGEX.match(physical_line).group(1) + if indent.count('\t'): + return indent.index('\t'), "W191 indentation contains tabs" + + +def trailing_whitespace(physical_line): + r""" + JCR: Trailing whitespace is superfluous. + FBM: Except when it occurs as part of a blank line (i.e. the line is + nothing but whitespace). According to Python docs[1] a line with only + whitespace is considered a blank line, and is to be ignored. However, + matching a blank line to its indentation level avoids mistakenly + terminating a multi-line statement (e.g. class declaration) when + pasting code into the standard Python interpreter. + + [1] http://docs.python.org/reference/lexical_analysis.html#blank-lines + + The warning returned varies on whether the line itself is blank, for easier + filtering for those who want to indent their blank lines. + + Okay: spam(1) + W291: spam(1)\s + W293: class Foo(object):\n \n bang = 12 + """ + physical_line = physical_line.rstrip('\n') # chr(10), newline + physical_line = physical_line.rstrip('\r') # chr(13), carriage return + physical_line = physical_line.rstrip('\x0c') # chr(12), form feed, ^L + stripped = physical_line.rstrip() + if physical_line != stripped: + if stripped: + return len(stripped), "W291 trailing whitespace" + else: + return 0, "W293 blank line contains whitespace" + + +def trailing_blank_lines(physical_line, lines, line_number): + r""" + JCR: Trailing blank lines are superfluous. + + Okay: spam(1) + W391: spam(1)\n + """ + if physical_line.strip() == '' and line_number == len(lines): + return 0, "W391 blank line at end of file" + + +def missing_newline(physical_line): + """ + JCR: The last line should have a newline. + """ + if physical_line.rstrip() == physical_line: + return len(physical_line), "W292 no newline at end of file" + + +def maximum_line_length(physical_line): + """ + Limit all lines to a maximum of 79 characters. + + There are still many devices around that are limited to 80 character + lines; plus, limiting windows to 80 characters makes it possible to have + several windows side-by-side. The default wrapping on such devices looks + ugly. Therefore, please limit all lines to a maximum of 79 characters. + For flowing long blocks of text (docstrings or comments), limiting the + length to 72 characters is recommended. + """ + line = physical_line.rstrip() + length = len(line) + if length > MAX_LINE_LENGTH: + try: + # The line could contain multi-byte characters + if not hasattr(line, 'decode'): # Python 3 + line = line.encode('latin-1') + length = len(line.decode('utf-8')) + except UnicodeDecodeError: + pass + if length > MAX_LINE_LENGTH: + return MAX_LINE_LENGTH, "E501 line too long (%d characters)" % length + + +############################################################################## +# Plugins (check functions) for logical lines +############################################################################## + + +def blank_lines(logical_line, blank_lines, indent_level, line_number, + previous_logical, previous_indent_level, + blank_lines_before_comment): + r""" + Separate top-level function and class definitions with two blank lines. + + Method definitions inside a class are separated by a single blank line. + + Extra blank lines may be used (sparingly) to separate groups of related + functions. Blank lines may be omitted between a bunch of related + one-liners (e.g. a set of dummy implementations). + + Use blank lines in functions, sparingly, to indicate logical sections. + + Okay: def a():\n pass\n\n\ndef b():\n pass + Okay: def a():\n pass\n\n\n# Foo\n# Bar\n\ndef b():\n pass + + E301: class Foo:\n b = 0\n def bar():\n pass + E302: def a():\n pass\n\ndef b(n):\n pass + E303: def a():\n pass\n\n\n\ndef b(n):\n pass + E303: def a():\n\n\n\n pass + E304: @decorator\n\ndef a():\n pass + """ + if line_number == 1: + return # Don't expect blank lines before the first line + max_blank_lines = max(blank_lines, blank_lines_before_comment) + if previous_logical.startswith('@'): + if max_blank_lines: + return 0, "E304 blank lines found after function decorator" + elif max_blank_lines > 2 or (indent_level and max_blank_lines == 2): + return 0, "E303 too many blank lines (%d)" % max_blank_lines + elif (logical_line.startswith('def ') or + logical_line.startswith('class ') or + logical_line.startswith('@')): + if indent_level: + if not (max_blank_lines or previous_indent_level < indent_level or + DOCSTRING_REGEX.match(previous_logical)): + return 0, "E301 expected 1 blank line, found 0" + elif max_blank_lines != 2: + return 0, "E302 expected 2 blank lines, found %d" % max_blank_lines + + +def extraneous_whitespace(logical_line): + """ + Avoid extraneous whitespace in the following situations: + + - Immediately inside parentheses, brackets or braces. + + - Immediately before a comma, semicolon, or colon. + + Okay: spam(ham[1], {eggs: 2}) + E201: spam( ham[1], {eggs: 2}) + E201: spam(ham[ 1], {eggs: 2}) + E201: spam(ham[1], { eggs: 2}) + E202: spam(ham[1], {eggs: 2} ) + E202: spam(ham[1 ], {eggs: 2}) + E202: spam(ham[1], {eggs: 2 }) + + E203: if x == 4: print x, y; x, y = y , x + E203: if x == 4: print x, y ; x, y = y, x + E203: if x == 4 : print x, y; x, y = y, x + """ + line = logical_line + for match in EXTRANEOUS_WHITESPACE_REGEX.finditer(line): + text = match.group() + char = text.strip() + found = match.start() + if text == char + ' ' and char in '([{': + return found + 1, "E201 whitespace after '%s'" % char + if text == ' ' + char and line[found - 1] != ',': + if char in '}])': + return found, "E202 whitespace before '%s'" % char + if char in ',;:': + return found, "E203 whitespace before '%s'" % char + + +def missing_whitespace(logical_line): + """ + JCR: Each comma, semicolon or colon should be followed by whitespace. + + Okay: [a, b] + Okay: (3,) + Okay: a[1:4] + Okay: a[:4] + Okay: a[1:] + Okay: a[1:4:2] + E231: ['a','b'] + E231: foo(bar,baz) + """ + line = logical_line + for index in range(len(line) - 1): + char = line[index] + if char in ',;:' and line[index + 1] not in WHITESPACE: + before = line[:index] + if char == ':' and before.count('[') > before.count(']'): + continue # Slice syntax, no space required + if char == ',' and line[index + 1] == ')': + continue # Allow tuple with only one element: (3,) + return index, "E231 missing whitespace after '%s'" % char + + +def indentation(logical_line, previous_logical, indent_char, + indent_level, previous_indent_level): + r""" + Use 4 spaces per indentation level. + + For really old code that you don't want to mess up, you can continue to + use 8-space tabs. + + Okay: a = 1 + Okay: if a == 0:\n a = 1 + E111: a = 1 + + Okay: for item in items:\n pass + E112: for item in items:\npass + + Okay: a = 1\nb = 2 + E113: a = 1\n b = 2 + """ + if indent_char == ' ' and indent_level % 4: + return 0, "E111 indentation is not a multiple of four" + indent_expect = previous_logical.endswith(':') + if indent_expect and indent_level <= previous_indent_level: + return 0, "E112 expected an indented block" + if indent_level > previous_indent_level and not indent_expect: + return 0, "E113 unexpected indentation" + + +def whitespace_before_parameters(logical_line, tokens): + """ + Avoid extraneous whitespace in the following situations: + + - Immediately before the open parenthesis that starts the argument + list of a function call. + + - Immediately before the open parenthesis that starts an indexing or + slicing. + + Okay: spam(1) + E211: spam (1) + + Okay: dict['key'] = list[index] + E211: dict ['key'] = list[index] + E211: dict['key'] = list [index] + """ + prev_type = tokens[0][0] + prev_text = tokens[0][1] + prev_end = tokens[0][3] + for index in range(1, len(tokens)): + token_type, text, start, end, line = tokens[index] + if (token_type == tokenize.OP and + text in '([' and + start != prev_end and + (prev_type == tokenize.NAME or prev_text in '}])') and + # Syntax "class A (B):" is allowed, but avoid it + (index < 2 or tokens[index - 2][1] != 'class') and + # Allow "return (a.foo for a in range(5))" + (not keyword.iskeyword(prev_text))): + return prev_end, "E211 whitespace before '%s'" % text + prev_type = token_type + prev_text = text + prev_end = end + + +def whitespace_around_operator(logical_line): + """ + Avoid extraneous whitespace in the following situations: + + - More than one space around an assignment (or other) operator to + align it with another. + + Okay: a = 12 + 3 + E221: a = 4 + 5 + E222: a = 4 + 5 + E223: a = 4\t+ 5 + E224: a = 4 +\t5 + """ + for match in WHITESPACE_AROUND_OPERATOR_REGEX.finditer(logical_line): + before, whitespace, after = match.groups() + tab = whitespace == '\t' + offset = match.start(2) + if before in OPERATORS: + return offset, (tab and "E224 tab after operator" or + "E222 multiple spaces after operator") + elif after in OPERATORS: + return offset, (tab and "E223 tab before operator" or + "E221 multiple spaces before operator") + + +def missing_whitespace_around_operator(logical_line, tokens): + r""" + - Always surround these binary operators with a single space on + either side: assignment (=), augmented assignment (+=, -= etc.), + comparisons (==, <, >, !=, <>, <=, >=, in, not in, is, is not), + Booleans (and, or, not). + + - Use spaces around arithmetic operators. + + Okay: i = i + 1 + Okay: submitted += 1 + Okay: x = x * 2 - 1 + Okay: hypot2 = x * x + y * y + Okay: c = (a + b) * (a - b) + Okay: foo(bar, key='word', *args, **kwargs) + Okay: baz(**kwargs) + Okay: negative = -1 + Okay: spam(-1) + Okay: alpha[:-i] + Okay: if not -5 < x < +5:\n pass + Okay: lambda *args, **kw: (args, kw) + + E225: i=i+1 + E225: submitted +=1 + E225: x = x*2 - 1 + E225: hypot2 = x*x + y*y + E225: c = (a+b) * (a-b) + E225: c = alpha -4 + E225: z = x **y + """ + parens = 0 + need_space = False + prev_type = tokenize.OP + prev_text = prev_end = None + for token_type, text, start, end, line in tokens: + if token_type in (tokenize.NL, tokenize.NEWLINE, tokenize.ERRORTOKEN): + # ERRORTOKEN is triggered by backticks in Python 3000 + continue + if text in ('(', 'lambda'): + parens += 1 + elif text == ')': + parens -= 1 + if need_space: + if start != prev_end: + need_space = False + elif text == '>' and prev_text == '<': + # Tolerate the "<>" operator, even if running Python 3 + pass + else: + return prev_end, "E225 missing whitespace around operator" + elif token_type == tokenize.OP and prev_end is not None: + if text == '=' and parens: + # Allow keyword args or defaults: foo(bar=None). + pass + elif text in BINARY_OPERATORS: + need_space = True + elif text in UNARY_OPERATORS: + # Allow unary operators: -123, -x, +1. + # Allow argument unpacking: foo(*args, **kwargs). + if prev_type == tokenize.OP: + if prev_text in '}])': + need_space = True + elif prev_type == tokenize.NAME: + if prev_text not in E225NOT_KEYWORDS: + need_space = True + else: + need_space = True + if need_space and start == prev_end: + return prev_end, "E225 missing whitespace around operator" + prev_type = token_type + prev_text = text + prev_end = end + + +def whitespace_around_comma(logical_line): + """ + Avoid extraneous whitespace in the following situations: + + - More than one space around an assignment (or other) operator to + align it with another. + + JCR: This should also be applied around comma etc. + Note: these checks are disabled by default + + Okay: a = (1, 2) + E241: a = (1, 2) + E242: a = (1,\t2) + """ + line = logical_line + for separator in ',;:': + found = line.find(separator + ' ') + if found > -1: + return found + 1, "E241 multiple spaces after '%s'" % separator + found = line.find(separator + '\t') + if found > -1: + return found + 1, "E242 tab after '%s'" % separator + + +def whitespace_around_named_parameter_equals(logical_line): + """ + Don't use spaces around the '=' sign when used to indicate a + keyword argument or a default parameter value. + + Okay: def complex(real, imag=0.0): + Okay: return magic(r=real, i=imag) + Okay: boolean(a == b) + Okay: boolean(a != b) + Okay: boolean(a <= b) + Okay: boolean(a >= b) + + E251: def complex(real, imag = 0.0): + E251: return magic(r = real, i = imag) + """ + parens = 0 + for match in WHITESPACE_AROUND_NAMED_PARAMETER_REGEX.finditer( + logical_line): + text = match.group() + if parens and len(text) == 3: + issue = "E251 no spaces around keyword / parameter equals" + return match.start(), issue + if text == '(': + parens += 1 + elif text == ')': + parens -= 1 + + +def whitespace_before_inline_comment(logical_line, tokens): + """ + Separate inline comments by at least two spaces. + + An inline comment is a comment on the same line as a statement. Inline + comments should be separated by at least two spaces from the statement. + They should start with a # and a single space. + + Okay: x = x + 1 # Increment x + Okay: x = x + 1 # Increment x + E261: x = x + 1 # Increment x + E262: x = x + 1 #Increment x + E262: x = x + 1 # Increment x + """ + prev_end = (0, 0) + for token_type, text, start, end, line in tokens: + if token_type == tokenize.NL: + continue + if token_type == tokenize.COMMENT: + if not line[:start[1]].strip(): + continue + if prev_end[0] == start[0] and start[1] < prev_end[1] + 2: + return (prev_end, + "E261 at least two spaces before inline comment") + if (len(text) > 1 and text.startswith('# ') + or not text.startswith('# ')): + return start, "E262 inline comment should start with '# '" + else: + prev_end = end + + +def imports_on_separate_lines(logical_line): + r""" + Imports should usually be on separate lines. + + Okay: import os\nimport sys + E401: import sys, os + + Okay: from subprocess import Popen, PIPE + Okay: from myclas import MyClass + Okay: from foo.bar.yourclass import YourClass + Okay: import myclass + Okay: import foo.bar.yourclass + """ + line = logical_line + if line.startswith('import '): + found = line.find(',') + if found > -1: + return found, "E401 multiple imports on one line" + + +def compound_statements(logical_line): + r""" + Compound statements (multiple statements on the same line) are + generally discouraged. + + While sometimes it's okay to put an if/for/while with a small body + on the same line, never do this for multi-clause statements. Also + avoid folding such long lines! + + Okay: if foo == 'blah':\n do_blah_thing() + Okay: do_one() + Okay: do_two() + Okay: do_three() + + E701: if foo == 'blah': do_blah_thing() + E701: for x in lst: total += x + E701: while t < 10: t = delay() + E701: if foo == 'blah': do_blah_thing() + E701: else: do_non_blah_thing() + E701: try: something() + E701: finally: cleanup() + E701: if foo == 'blah': one(); two(); three() + + E702: do_one(); do_two(); do_three() + """ + line = logical_line + found = line.find(':') + if -1 < found < len(line) - 1: + before = line[:found] + if (before.count('{') <= before.count('}') and # {'a': 1} (dict) + before.count('[') <= before.count(']') and # [1:2] (slice) + not re.search(r'\blambda\b', before)): # lambda x: x + return found, "E701 multiple statements on one line (colon)" + found = line.find(';') + if -1 < found: + return found, "E702 multiple statements on one line (semicolon)" + + +def python_3000_has_key(logical_line): + """ + The {}.has_key() method will be removed in the future version of + Python. Use the 'in' operation instead, like: + d = {"a": 1, "b": 2} + if "b" in d: + print d["b"] + """ + pos = logical_line.find('.has_key(') + if pos > -1: + return pos, "W601 .has_key() is deprecated, use 'in'" + + +def python_3000_raise_comma(logical_line): + """ + When raising an exception, use "raise ValueError('message')" + instead of the older form "raise ValueError, 'message'". + + The paren-using form is preferred because when the exception arguments + are long or include string formatting, you don't need to use line + continuation characters thanks to the containing parentheses. The older + form will be removed in Python 3000. + """ + match = RAISE_COMMA_REGEX.match(logical_line) + if match: + return match.start(1), "W602 deprecated form of raising exception" + + +def python_3000_not_equal(logical_line): + """ + != can also be written <>, but this is an obsolete usage kept for + backwards compatibility only. New code should always use !=. + The older syntax is removed in Python 3000. + """ + pos = logical_line.find('<>') + if pos > -1: + return pos, "W603 '<>' is deprecated, use '!='" + + +def python_3000_backticks(logical_line): + """ + Backticks are removed in Python 3000. + Use repr() instead. + """ + pos = logical_line.find('`') + if pos > -1: + return pos, "W604 backticks are deprecated, use 'repr()'" + + +############################################################################## +# Helper functions +############################################################################## + + +if '' == ''.encode(): + # Python 2: implicit encoding. + def readlines(filename): + return open(filename).readlines() +else: + # Python 3: decode to latin-1. + # This function is lazy, it does not read the encoding declaration. + # XXX: use tokenize.detect_encoding() + def readlines(filename): + return open(filename, encoding='latin-1').readlines() + + +def expand_indent(line): + """ + Return the amount of indentation. + Tabs are expanded to the next multiple of 8. + + >>> expand_indent(' ') + 4 + >>> expand_indent('\\t') + 8 + >>> expand_indent(' \\t') + 8 + >>> expand_indent(' \\t') + 8 + >>> expand_indent(' \\t') + 16 + """ + result = 0 + for char in line: + if char == '\t': + result = result // 8 * 8 + 8 + elif char == ' ': + result += 1 + else: + break + return result + + +def mute_string(text): + """ + Replace contents with 'xxx' to prevent syntax matching. + + >>> mute_string('"abc"') + '"xxx"' + >>> mute_string("'''abc'''") + "'''xxx'''" + >>> mute_string("r'abc'") + "r'xxx'" + """ + start = 1 + end = len(text) - 1 + # String modifiers (e.g. u or r) + if text.endswith('"'): + start += text.index('"') + elif text.endswith("'"): + start += text.index("'") + # Triple quotes + if text.endswith('"""') or text.endswith("'''"): + start += 2 + end -= 2 + return text[:start] + 'x' * (end - start) + text[end:] + + +def message(text): + """Print a message.""" + # print >> sys.stderr, options.prog + ': ' + text + # print >> sys.stderr, text + print(text) + + +############################################################################## +# Framework to run all checks +############################################################################## + + +def find_checks(argument_name): + """ + Find all globally visible functions where the first argument name + starts with argument_name. + """ + checks = [] + for name, function in globals().items(): + if not inspect.isfunction(function): + continue + args = inspect.getargspec(function)[0] + if args and args[0].startswith(argument_name): + codes = ERRORCODE_REGEX.findall(inspect.getdoc(function) or '') + for code in codes or ['']: + if not code or not ignore_code(code): + checks.append((name, function, args)) + break + checks.sort() + return checks + + +class Checker(object): + """ + Load a Python source file, tokenize it, check coding style. + """ + + def __init__(self, filename, lines=None): + self.filename = filename + if filename is None: + self.filename = 'stdin' + self.lines = lines or [] + elif lines is None: + self.lines = readlines(filename) + else: + self.lines = lines + options.counters['physical lines'] += len(self.lines) + + def readline(self): + """ + Get the next line from the input buffer. + """ + self.line_number += 1 + if self.line_number > len(self.lines): + return '' + return self.lines[self.line_number - 1] + + def readline_check_physical(self): + """ + Check and return the next physical line. This method can be + used to feed tokenize.generate_tokens. + """ + line = self.readline() + if line: + self.check_physical(line) + return line + + def run_check(self, check, argument_names): + """ + Run a check plugin. + """ + arguments = [] + for name in argument_names: + arguments.append(getattr(self, name)) + return check(*arguments) + + def check_physical(self, line): + """ + Run all physical checks on a raw input line. + """ + self.physical_line = line + if self.indent_char is None and len(line) and line[0] in ' \t': + self.indent_char = line[0] + for name, check, argument_names in options.physical_checks: + result = self.run_check(check, argument_names) + if result is not None: + offset, text = result + self.report_error(self.line_number, offset, text, check) + + def build_tokens_line(self): + """ + Build a logical line from tokens. + """ + self.mapping = [] + logical = [] + length = 0 + previous = None + for token in self.tokens: + token_type, text = token[0:2] + if token_type in SKIP_TOKENS: + continue + if token_type == tokenize.STRING: + text = mute_string(text) + if previous: + end_line, end = previous[3] + start_line, start = token[2] + if end_line != start_line: # different row + prev_text = self.lines[end_line - 1][end - 1] + if prev_text == ',' or (prev_text not in '{[(' + and text not in '}])'): + logical.append(' ') + length += 1 + elif end != start: # different column + fill = self.lines[end_line - 1][end:start] + logical.append(fill) + length += len(fill) + self.mapping.append((length, token)) + logical.append(text) + length += len(text) + previous = token + self.logical_line = ''.join(logical) + assert self.logical_line.lstrip() == self.logical_line + assert self.logical_line.rstrip() == self.logical_line + + def check_logical(self): + """ + Build a line from tokens and run all logical checks on it. + """ + options.counters['logical lines'] += 1 + self.build_tokens_line() + first_line = self.lines[self.mapping[0][1][2][0] - 1] + indent = first_line[:self.mapping[0][1][2][1]] + self.previous_indent_level = self.indent_level + self.indent_level = expand_indent(indent) + if options.verbose >= 2: + print(self.logical_line[:80].rstrip()) + for name, check, argument_names in options.logical_checks: + if options.verbose >= 4: + print(' ' + name) + result = self.run_check(check, argument_names) + if result is not None: + offset, text = result + if isinstance(offset, tuple): + original_number, original_offset = offset + else: + for token_offset, token in self.mapping: + if offset >= token_offset: + original_number = token[2][0] + original_offset = (token[2][1] + + offset - token_offset) + self.report_error(original_number, original_offset, + text, check) + self.previous_logical = self.logical_line + + def check_all(self, expected=None, line_offset=0): + """ + Run all checks on the input file. + """ + self.expected = expected or () + self.line_offset = line_offset + self.line_number = 0 + self.file_errors = 0 + self.indent_char = None + self.indent_level = 0 + self.previous_logical = '' + self.blank_lines = 0 + self.blank_lines_before_comment = 0 + self.tokens = [] + parens = 0 + for token in tokenize.generate_tokens(self.readline_check_physical): + if options.verbose >= 3: + if token[2][0] == token[3][0]: + pos = '[%s:%s]' % (token[2][1] or '', token[3][1]) + else: + pos = 'l.%s' % token[3][0] + print('l.%s\t%s\t%s\t%r' % + (token[2][0], pos, tokenize.tok_name[token[0]], token[1])) + self.tokens.append(token) + token_type, text = token[0:2] + if token_type == tokenize.OP and text in '([{': + parens += 1 + if token_type == tokenize.OP and text in '}])': + parens -= 1 + if token_type == tokenize.NEWLINE and not parens: + self.check_logical() + self.blank_lines = 0 + self.blank_lines_before_comment = 0 + self.tokens = [] + if token_type == tokenize.NL and not parens: + if len(self.tokens) <= 1: + # The physical line contains only this token. + self.blank_lines += 1 + self.tokens = [] + if token_type == tokenize.COMMENT: + source_line = token[4] + token_start = token[2][1] + if source_line[:token_start].strip() == '': + self.blank_lines_before_comment = max(self.blank_lines, + self.blank_lines_before_comment) + self.blank_lines = 0 + if text.endswith('\n') and not parens: + # The comment also ends a physical line. This works around + # Python < 2.6 behaviour, which does not generate NL after + # a comment which is on a line by itself. + self.tokens = [] + return self.file_errors + + def report_error(self, line_number, offset, text, check): + """ + Report an error, according to options. + """ + code = text[:4] + if ignore_code(code): + return + if options.quiet == 1 and not self.file_errors: + message(self.filename) + if code in options.counters: + options.counters[code] += 1 + else: + options.counters[code] = 1 + options.messages[code] = text[5:] + if options.quiet or code in self.expected: + # Don't care about expected errors or warnings + return + self.file_errors += 1 + if options.counters[code] == 1 or options.repeat: + message("%s:%s:%d: %s" % + (self.filename, self.line_offset + line_number, + offset + 1, text)) + if options.show_source: + line = self.lines[line_number - 1] + message(line.rstrip()) + message(' ' * offset + '^') + if options.show_pep8: + message(check.__doc__.lstrip('\n').rstrip()) + + +def input_file(filename): + """ + Run all checks on a Python source file. + """ + if options.verbose: + message('checking ' + filename) + errors = Checker(filename).check_all() + + +def input_dir(dirname, runner=None): + """ + Check all Python source files in this directory and all subdirectories. + """ + dirname = dirname.rstrip('/') + if excluded(dirname): + return + if runner is None: + runner = input_file + for root, dirs, files in os.walk(dirname): + if options.verbose: + message('directory ' + root) + options.counters['directories'] += 1 + dirs.sort() + for subdir in dirs: + if excluded(subdir): + dirs.remove(subdir) + files.sort() + for filename in files: + if filename_match(filename) and not excluded(filename): + options.counters['files'] += 1 + runner(os.path.join(root, filename)) + + +def excluded(filename): + """ + Check if options.exclude contains a pattern that matches filename. + """ + basename = os.path.basename(filename) + for pattern in options.exclude: + if fnmatch(basename, pattern): + # print basename, 'excluded because it matches', pattern + return True + + +def filename_match(filename): + """ + Check if options.filename contains a pattern that matches filename. + If options.filename is unspecified, this always returns True. + """ + if not options.filename: + return True + for pattern in options.filename: + if fnmatch(filename, pattern): + return True + + +def ignore_code(code): + """ + Check if options.ignore contains a prefix of the error code. + If options.select contains a prefix of the error code, do not ignore it. + """ + for select in options.select: + if code.startswith(select): + return False + for ignore in options.ignore: + if code.startswith(ignore): + return True + + +def reset_counters(): + for key in list(options.counters.keys()): + if key not in BENCHMARK_KEYS: + del options.counters[key] + options.messages = {} + + +def get_error_statistics(): + """Get error statistics.""" + return get_statistics("E") + + +def get_warning_statistics(): + """Get warning statistics.""" + return get_statistics("W") + + +def get_statistics(prefix=''): + """ + Get statistics for message codes that start with the prefix. + + prefix='' matches all errors and warnings + prefix='E' matches all errors + prefix='W' matches all warnings + prefix='E4' matches all errors that have to do with imports + """ + stats = [] + keys = list(options.messages.keys()) + keys.sort() + for key in keys: + if key.startswith(prefix): + stats.append('%-7s %s %s' % + (options.counters[key], key, options.messages[key])) + return stats + + +def get_count(prefix=''): + """Return the total count of errors and warnings.""" + keys = list(options.messages.keys()) + count = 0 + for key in keys: + if key.startswith(prefix): + count += options.counters[key] + return count + + +def print_statistics(prefix=''): + """Print overall statistics (number of errors and warnings).""" + for line in get_statistics(prefix): + print(line) + + +def print_benchmark(elapsed): + """ + Print benchmark numbers. + """ + print('%-7.2f %s' % (elapsed, 'seconds elapsed')) + for key in BENCHMARK_KEYS: + print('%-7d %s per second (%d total)' % ( + options.counters[key] / elapsed, key, + options.counters[key])) + + +def run_tests(filename): + """ + Run all the tests from a file. + + A test file can provide many tests. Each test starts with a declaration. + This declaration is a single line starting with '#:'. + It declares codes of expected failures, separated by spaces or 'Okay' + if no failure is expected. + If the file does not contain such declaration, it should pass all tests. + If the declaration is empty, following lines are not checked, until next + declaration. + + Examples: + + * Only E224 and W701 are expected: #: E224 W701 + * Following example is conform: #: Okay + * Don't check these lines: #: + """ + lines = readlines(filename) + ['#:\n'] + line_offset = 0 + codes = ['Okay'] + testcase = [] + for index, line in enumerate(lines): + if not line.startswith('#:'): + if codes: + # Collect the lines of the test case + testcase.append(line) + continue + if codes and index > 0: + label = '%s:%s:1' % (filename, line_offset + 1) + codes = [c for c in codes if c != 'Okay'] + # Run the checker + errors = Checker(filename, testcase).check_all(codes, line_offset) + # Check if the expected errors were found + for code in codes: + if not options.counters.get(code): + errors += 1 + message('%s: error %s not found' % (label, code)) + if options.verbose and not errors: + message('%s: passed (%s)' % (label, ' '.join(codes))) + # Keep showing errors for multiple tests + reset_counters() + # output the real line numbers + line_offset = index + # configure the expected errors + codes = line.split()[1:] + # empty the test case buffer + del testcase[:] + + +def selftest(): + """ + Test all check functions with test cases in docstrings. + """ + count_passed = 0 + count_failed = 0 + checks = options.physical_checks + options.logical_checks + for name, check, argument_names in checks: + for line in check.__doc__.splitlines(): + line = line.lstrip() + match = SELFTEST_REGEX.match(line) + if match is None: + continue + code, source = match.groups() + checker = Checker(None) + for part in source.split(r'\n'): + part = part.replace(r'\t', '\t') + part = part.replace(r'\s', ' ') + checker.lines.append(part + '\n') + options.quiet = 2 + checker.check_all() + error = None + if code == 'Okay': + if len(options.counters) > len(BENCHMARK_KEYS): + codes = [key for key in options.counters.keys() + if key not in BENCHMARK_KEYS] + error = "incorrectly found %s" % ', '.join(codes) + elif not options.counters.get(code): + error = "failed to find %s" % code + # Reset the counters + reset_counters() + if not error: + count_passed += 1 + else: + count_failed += 1 + if len(checker.lines) == 1: + print("pep8.py: %s: %s" % + (error, checker.lines[0].rstrip())) + else: + print("pep8.py: %s:" % error) + for line in checker.lines: + print(line.rstrip()) + if options.verbose: + print("%d passed and %d failed." % (count_passed, count_failed)) + if count_failed: + print("Test failed.") + else: + print("Test passed.") + + +def process_options(arglist=None): + """ + Process options passed either via arglist or via command line args. + """ + global options, args + parser = OptionParser(version=__version__, + usage="%prog [options] input ...") + parser.add_option('-v', '--verbose', default=0, action='count', + help="print status messages, or debug with -vv") + parser.add_option('-q', '--quiet', default=0, action='count', + help="report only file names, or nothing with -qq") + parser.add_option('-r', '--repeat', action='store_true', + help="show all occurrences of the same error") + parser.add_option('--exclude', metavar='patterns', default=DEFAULT_EXCLUDE, + help="exclude files or directories which match these " + "comma separated patterns (default: %s)" % + DEFAULT_EXCLUDE) + parser.add_option('--filename', metavar='patterns', default='*.py', + help="when parsing directories, only check filenames " + "matching these comma separated patterns (default: " + "*.py)") + parser.add_option('--select', metavar='errors', default='', + help="select errors and warnings (e.g. E,W6)") + parser.add_option('--ignore', metavar='errors', default='', + help="skip errors and warnings (e.g. E4,W)") + parser.add_option('--show-source', action='store_true', + help="show source code for each error") + parser.add_option('--show-pep8', action='store_true', + help="show text of PEP 8 for each error") + parser.add_option('--statistics', action='store_true', + help="count errors and warnings") + parser.add_option('--count', action='store_true', + help="print total number of errors and warnings " + "to standard error and set exit code to 1 if " + "total is not null") + parser.add_option('--benchmark', action='store_true', + help="measure processing speed") + parser.add_option('--testsuite', metavar='dir', + help="run regression tests from dir") + parser.add_option('--doctest', action='store_true', + help="run doctest on myself") + options, args = parser.parse_args(arglist) + if options.testsuite: + args.append(options.testsuite) + if not args and not options.doctest: + parser.error('input not specified') + options.prog = os.path.basename(sys.argv[0]) + options.exclude = options.exclude.split(',') + for index in range(len(options.exclude)): + options.exclude[index] = options.exclude[index].rstrip('/') + if options.filename: + options.filename = options.filename.split(',') + if options.select: + options.select = options.select.split(',') + else: + options.select = [] + if options.ignore: + options.ignore = options.ignore.split(',') + elif options.select: + # Ignore all checks which are not explicitly selected + options.ignore = [''] + elif options.testsuite or options.doctest: + # For doctest and testsuite, all checks are required + options.ignore = [] + else: + # The default choice: ignore controversial checks + options.ignore = DEFAULT_IGNORE.split(',') + options.physical_checks = find_checks('physical_line') + options.logical_checks = find_checks('logical_line') + options.counters = dict.fromkeys(BENCHMARK_KEYS, 0) + options.messages = {} + return options, args + + +def _main(): + """ + Parse options and run checks on Python source. + """ + options, args = process_options() + if options.doctest: + import doctest + doctest.testmod(verbose=options.verbose) + selftest() + if options.testsuite: + runner = run_tests + else: + runner = input_file + start_time = time.time() + for path in args: + if os.path.isdir(path): + input_dir(path, runner=runner) + elif not excluded(path): + options.counters['files'] += 1 + runner(path) + elapsed = time.time() - start_time + if options.statistics: + print_statistics() + if options.benchmark: + print_benchmark(elapsed) + count = get_count() + if count: + if options.count: + sys.stderr.write(str(count) + '\n') + sys.exit(1) + + +if __name__ == '__main__': + _main() diff --git a/sublimelinter/modules/libs/pyflakes/__init__.py b/sublimelinter/modules/libs/pyflakes/__init__.py new file mode 100644 index 00000000..abeeedbf --- /dev/null +++ b/sublimelinter/modules/libs/pyflakes/__init__.py @@ -0,0 +1 @@ +__version__ = '0.4.0' diff --git a/sublimelinter/modules/libs/pyflakes/checker.py b/sublimelinter/modules/libs/pyflakes/checker.py new file mode 100644 index 00000000..fa2494e0 --- /dev/null +++ b/sublimelinter/modules/libs/pyflakes/checker.py @@ -0,0 +1,635 @@ +# -*- test-case-name: pyflakes -*- +# (c) 2005-2010 Divmod, Inc. +# See LICENSE file for details + +import __builtin__ +import os.path +import _ast + +from pyflakes import messages + + +# utility function to iterate over an AST node's children, adapted +# from Python 2.6's standard ast module +try: + import ast + iter_child_nodes = ast.iter_child_nodes +except (ImportError, AttributeError): + def iter_child_nodes(node, astcls=_ast.AST): + """ + Yield all direct child nodes of *node*, that is, all fields that are nodes + and all items of fields that are lists of nodes. + """ + for name in node._fields: + field = getattr(node, name, None) + if isinstance(field, astcls): + yield field + elif isinstance(field, list): + for item in field: + yield item + + +class Binding(object): + """ + Represents the binding of a value to a name. + + The checker uses this to keep track of which names have been bound and + which names have not. See L{Assignment} for a special type of binding that + is checked with stricter rules. + + @ivar used: pair of (L{Scope}, line-number) indicating the scope and + line number that this binding was last used + """ + + def __init__(self, name, source): + self.name = name + self.source = source + self.used = False + + + def __str__(self): + return self.name + + + def __repr__(self): + return '<%s object %r from line %r at 0x%x>' % (self.__class__.__name__, + self.name, + self.source.lineno, + id(self)) + + + +class UnBinding(Binding): + '''Created by the 'del' operator.''' + + + +class Importation(Binding): + """ + A binding created by an import statement. + + @ivar fullName: The complete name given to the import statement, + possibly including multiple dotted components. + @type fullName: C{str} + """ + def __init__(self, name, source): + self.fullName = name + name = name.split('.')[0] + super(Importation, self).__init__(name, source) + + + +class Argument(Binding): + """ + Represents binding a name as an argument. + """ + + + +class Assignment(Binding): + """ + Represents binding a name with an explicit assignment. + + The checker will raise warnings for any Assignment that isn't used. Also, + the checker does not consider assignments in tuple/list unpacking to be + Assignments, rather it treats them as simple Bindings. + """ + + + +class FunctionDefinition(Binding): + _property_decorator = False + + + +class ExportBinding(Binding): + """ + A binding created by an C{__all__} assignment. If the names in the list + can be determined statically, they will be treated as names for export and + additional checking applied to them. + + The only C{__all__} assignment that can be recognized is one which takes + the value of a literal list containing literal strings. For example:: + + __all__ = ["foo", "bar"] + + Names which are imported and not otherwise used but appear in the value of + C{__all__} will not have an unused import warning reported for them. + """ + def names(self): + """ + Return a list of the names referenced by this binding. + """ + names = [] + if isinstance(self.source, _ast.List): + for node in self.source.elts: + if isinstance(node, _ast.Str): + names.append(node.s) + return names + + + +class Scope(dict): + importStarred = False # set to True when import * is found + + + def __repr__(self): + return '<%s at 0x%x %s>' % (self.__class__.__name__, id(self), dict.__repr__(self)) + + + def __init__(self): + super(Scope, self).__init__() + + + +class ClassScope(Scope): + pass + + + +class FunctionScope(Scope): + """ + I represent a name scope for a function. + + @ivar globals: Names declared 'global' in this function. + """ + def __init__(self): + super(FunctionScope, self).__init__() + self.globals = {} + + + +class ModuleScope(Scope): + pass + + +# Globally defined names which are not attributes of the __builtin__ module. +_MAGIC_GLOBALS = ['__file__', '__builtins__'] + + + +class Checker(object): + """ + I check the cleanliness and sanity of Python code. + + @ivar _deferredFunctions: Tracking list used by L{deferFunction}. Elements + of the list are two-tuples. The first element is the callable passed + to L{deferFunction}. The second element is a copy of the scope stack + at the time L{deferFunction} was called. + + @ivar _deferredAssignments: Similar to C{_deferredFunctions}, but for + callables which are deferred assignment checks. + """ + + nodeDepth = 0 + traceTree = False + + def __init__(self, tree, filename=None): + if filename is None: + filename = '(none)' + self._deferredFunctions = [] + self._deferredAssignments = [] + self.dead_scopes = [] + self.messages = [] + self.filename = filename + self.scopeStack = [ModuleScope()] + self.futuresAllowed = True + self.handleChildren(tree) + self._runDeferred(self._deferredFunctions) + # Set _deferredFunctions to None so that deferFunction will fail + # noisily if called after we've run through the deferred functions. + self._deferredFunctions = None + self._runDeferred(self._deferredAssignments) + # Set _deferredAssignments to None so that deferAssignment will fail + # noisly if called after we've run through the deferred assignments. + self._deferredAssignments = None + del self.scopeStack[1:] + self.popScope() + self.check_dead_scopes() + + + def deferFunction(self, callable): + ''' + Schedule a function handler to be called just before completion. + + This is used for handling function bodies, which must be deferred + because code later in the file might modify the global scope. When + `callable` is called, the scope at the time this is called will be + restored, however it will contain any new bindings added to it. + ''' + self._deferredFunctions.append((callable, self.scopeStack[:])) + + + def deferAssignment(self, callable): + """ + Schedule an assignment handler to be called just after deferred + function handlers. + """ + self._deferredAssignments.append((callable, self.scopeStack[:])) + + + def _runDeferred(self, deferred): + """ + Run the callables in C{deferred} using their associated scope stack. + """ + for handler, scope in deferred: + self.scopeStack = scope + handler() + + + def scope(self): + return self.scopeStack[-1] + scope = property(scope) + + def popScope(self): + self.dead_scopes.append(self.scopeStack.pop()) + + + def check_dead_scopes(self): + """ + Look at scopes which have been fully examined and report names in them + which were imported but unused. + """ + for scope in self.dead_scopes: + export = isinstance(scope.get('__all__'), ExportBinding) + if export: + all = scope['__all__'].names() + if os.path.split(self.filename)[1] != '__init__.py': + # Look for possible mistakes in the export list + undefined = set(all) - set(scope) + for name in undefined: + self.report( + messages.UndefinedExport, + scope['__all__'].source, + name) + else: + all = [] + + # Look for imported names that aren't used. + for importation in scope.itervalues(): + if isinstance(importation, Importation): + if not importation.used and importation.name not in all: + self.report( + messages.UnusedImport, + importation.source, + importation.name) + + + def pushFunctionScope(self): + self.scopeStack.append(FunctionScope()) + + def pushClassScope(self): + self.scopeStack.append(ClassScope()) + + def report(self, messageClass, *args, **kwargs): + self.messages.append(messageClass(self.filename, *args, **kwargs)) + + def handleChildren(self, tree): + for node in iter_child_nodes(tree): + self.handleNode(node, tree) + + def isDocstring(self, node): + """ + Determine if the given node is a docstring, as long as it is at the + correct place in the node tree. + """ + return isinstance(node, _ast.Str) or \ + (isinstance(node, _ast.Expr) and + isinstance(node.value, _ast.Str)) + + def handleNode(self, node, parent): + node.parent = parent + if self.traceTree: + print ' ' * self.nodeDepth + node.__class__.__name__ + self.nodeDepth += 1 + if self.futuresAllowed and not \ + (isinstance(node, _ast.ImportFrom) or self.isDocstring(node)): + self.futuresAllowed = False + nodeType = node.__class__.__name__.upper() + try: + handler = getattr(self, nodeType) + handler(node) + finally: + self.nodeDepth -= 1 + if self.traceTree: + print ' ' * self.nodeDepth + 'end ' + node.__class__.__name__ + + def ignore(self, node): + pass + + # "stmt" type nodes + RETURN = DELETE = PRINT = WHILE = IF = WITH = RAISE = TRYEXCEPT = \ + TRYFINALLY = ASSERT = EXEC = EXPR = handleChildren + + CONTINUE = BREAK = PASS = ignore + + # "expr" type nodes + BOOLOP = BINOP = UNARYOP = IFEXP = DICT = SET = YIELD = COMPARE = \ + CALL = REPR = ATTRIBUTE = SUBSCRIPT = LIST = TUPLE = handleChildren + + NUM = STR = ELLIPSIS = ignore + + # "slice" type nodes + SLICE = EXTSLICE = INDEX = handleChildren + + # expression contexts are node instances too, though being constants + LOAD = STORE = DEL = AUGLOAD = AUGSTORE = PARAM = ignore + + # same for operators + AND = OR = ADD = SUB = MULT = DIV = MOD = POW = LSHIFT = RSHIFT = \ + BITOR = BITXOR = BITAND = FLOORDIV = INVERT = NOT = UADD = USUB = \ + EQ = NOTEQ = LT = LTE = GT = GTE = IS = ISNOT = IN = NOTIN = ignore + + # additional node types + COMPREHENSION = EXCEPTHANDLER = KEYWORD = handleChildren + + def addBinding(self, loc, value, reportRedef=True): + '''Called when a binding is altered. + + - `loc` is the location (an object with lineno and optionally + col_offset attributes) of the statement responsible for the change + - `value` is the optional new value, a Binding instance, associated + with the binding; if None, the binding is deleted if it exists. + - if `reportRedef` is True (default), rebinding while unused will be + reported. + ''' + if (isinstance(self.scope.get(value.name), FunctionDefinition) + and isinstance(value, FunctionDefinition)): + if not value._property_decorator: + self.report(messages.RedefinedFunction, + loc, value.name, self.scope[value.name].source) + + if not isinstance(self.scope, ClassScope): + for scope in self.scopeStack[::-1]: + existing = scope.get(value.name) + if (isinstance(existing, Importation) + and not existing.used + and (not isinstance(value, Importation) or value.fullName == existing.fullName) + and reportRedef): + + self.report(messages.RedefinedWhileUnused, + loc, value.name, scope[value.name].source) + + if isinstance(value, UnBinding): + try: + del self.scope[value.name] + except KeyError: + self.report(messages.UndefinedName, loc, value.name) + else: + self.scope[value.name] = value + + def GLOBAL(self, node): + """ + Keep track of globals declarations. + """ + if isinstance(self.scope, FunctionScope): + self.scope.globals.update(dict.fromkeys(node.names)) + + def LISTCOMP(self, node): + # handle generators before element + for gen in node.generators: + self.handleNode(gen, node) + self.handleNode(node.elt, node) + + GENERATOREXP = SETCOMP = LISTCOMP + + # dictionary comprehensions; introduced in Python 2.7 + def DICTCOMP(self, node): + for gen in node.generators: + self.handleNode(gen, node) + self.handleNode(node.key, node) + self.handleNode(node.value, node) + + def FOR(self, node): + """ + Process bindings for loop variables. + """ + vars = [] + def collectLoopVars(n): + if isinstance(n, _ast.Name): + vars.append(n.id) + elif isinstance(n, _ast.expr_context): + return + else: + for c in iter_child_nodes(n): + collectLoopVars(c) + + collectLoopVars(node.target) + for varn in vars: + if (isinstance(self.scope.get(varn), Importation) + # unused ones will get an unused import warning + and self.scope[varn].used): + self.report(messages.ImportShadowedByLoopVar, + node, varn, self.scope[varn].source) + + self.handleChildren(node) + + def NAME(self, node): + """ + Handle occurrence of Name (which can be a load/store/delete access.) + """ + # Locate the name in locals / function / globals scopes. + if isinstance(node.ctx, (_ast.Load, _ast.AugLoad)): + # try local scope + importStarred = self.scope.importStarred + try: + self.scope[node.id].used = (self.scope, node) + except KeyError: + pass + else: + return + + # try enclosing function scopes + + for scope in self.scopeStack[-2:0:-1]: + importStarred = importStarred or scope.importStarred + if not isinstance(scope, FunctionScope): + continue + try: + scope[node.id].used = (self.scope, node) + except KeyError: + pass + else: + return + + # try global scope + + importStarred = importStarred or self.scopeStack[0].importStarred + try: + self.scopeStack[0][node.id].used = (self.scope, node) + except KeyError: + if ((not hasattr(__builtin__, node.id)) + and node.id not in _MAGIC_GLOBALS + and not importStarred): + if (os.path.basename(self.filename) == '__init__.py' and + node.id == '__path__'): + # the special name __path__ is valid only in packages + pass + else: + self.report(messages.UndefinedName, node, node.id) + elif isinstance(node.ctx, (_ast.Store, _ast.AugStore)): + # if the name hasn't already been defined in the current scope + if isinstance(self.scope, FunctionScope) and node.id not in self.scope: + # for each function or module scope above us + for scope in self.scopeStack[:-1]: + if not isinstance(scope, (FunctionScope, ModuleScope)): + continue + # if the name was defined in that scope, and the name has + # been accessed already in the current scope, and hasn't + # been declared global + if (node.id in scope + and scope[node.id].used + and scope[node.id].used[0] is self.scope + and node.id not in self.scope.globals): + # then it's probably a mistake + self.report(messages.UndefinedLocal, + scope[node.id].used[1], + node.id, + scope[node.id].source) + break + + if isinstance(node.parent, + (_ast.For, _ast.comprehension, _ast.Tuple, _ast.List)): + binding = Binding(node.id, node) + elif (node.id == '__all__' and + isinstance(self.scope, ModuleScope)): + binding = ExportBinding(node.id, node.parent.value) + else: + binding = Assignment(node.id, node) + if node.id in self.scope: + binding.used = self.scope[node.id].used + self.addBinding(node, binding) + elif isinstance(node.ctx, _ast.Del): + if isinstance(self.scope, FunctionScope) and \ + node.id in self.scope.globals: + del self.scope.globals[node.id] + else: + self.addBinding(node, UnBinding(node.id, node)) + else: + # must be a Param context -- this only happens for names in function + # arguments, but these aren't dispatched through here + raise RuntimeError( + "Got impossible expression context: %r" % (node.ctx,)) + + + def FUNCTIONDEF(self, node): + # the decorators attribute is called decorator_list as of Python 2.6 + if hasattr(node, 'decorators'): + for deco in node.decorators: + self.handleNode(deco, node) + else: + for deco in node.decorator_list: + self.handleNode(deco, node) + + # Check for property decorator + func_def = FunctionDefinition(node.name, node) + for decorator in node.decorator_list: + if getattr(decorator, 'attr', None) in ('setter', 'deleter'): + func_def._property_decorator = True + + self.addBinding(node, func_def) + self.LAMBDA(node) + + def LAMBDA(self, node): + for default in node.args.defaults: + self.handleNode(default, node) + + def runFunction(): + args = [] + + def addArgs(arglist): + for arg in arglist: + if isinstance(arg, _ast.Tuple): + addArgs(arg.elts) + else: + if arg.id in args: + self.report(messages.DuplicateArgument, + node, arg.id) + args.append(arg.id) + + self.pushFunctionScope() + addArgs(node.args.args) + # vararg/kwarg identifiers are not Name nodes + if node.args.vararg: + args.append(node.args.vararg) + if node.args.kwarg: + args.append(node.args.kwarg) + for name in args: + self.addBinding(node, Argument(name, node), reportRedef=False) + if isinstance(node.body, list): + # case for FunctionDefs + for stmt in node.body: + self.handleNode(stmt, node) + else: + # case for Lambdas + self.handleNode(node.body, node) + def checkUnusedAssignments(): + """ + Check to see if any assignments have not been used. + """ + for name, binding in self.scope.iteritems(): + if (not binding.used and not name in self.scope.globals + and isinstance(binding, Assignment)): + self.report(messages.UnusedVariable, + binding.source, name) + self.deferAssignment(checkUnusedAssignments) + self.popScope() + + self.deferFunction(runFunction) + + + def CLASSDEF(self, node): + """ + Check names used in a class definition, including its decorators, base + classes, and the body of its definition. Additionally, add its name to + the current scope. + """ + # decorator_list is present as of Python 2.6 + for deco in getattr(node, 'decorator_list', []): + self.handleNode(deco, node) + for baseNode in node.bases: + self.handleNode(baseNode, node) + self.pushClassScope() + for stmt in node.body: + self.handleNode(stmt, node) + self.popScope() + self.addBinding(node, Binding(node.name, node)) + + def ASSIGN(self, node): + self.handleNode(node.value, node) + for target in node.targets: + self.handleNode(target, node) + + def AUGASSIGN(self, node): + # AugAssign is awkward: must set the context explicitly and visit twice, + # once with AugLoad context, once with AugStore context + node.target.ctx = _ast.AugLoad() + self.handleNode(node.target, node) + self.handleNode(node.value, node) + node.target.ctx = _ast.AugStore() + self.handleNode(node.target, node) + + def IMPORT(self, node): + for alias in node.names: + name = alias.asname or alias.name + importation = Importation(name, node) + self.addBinding(node, importation) + + def IMPORTFROM(self, node): + if node.module == '__future__': + if not self.futuresAllowed: + self.report(messages.LateFutureImport, node, + [n.name for n in node.names]) + else: + self.futuresAllowed = False + + for alias in node.names: + if alias.name == '*': + self.scope.importStarred = True + self.report(messages.ImportStarUsed, node, node.module) + continue + name = alias.asname or alias.name + importation = Importation(name, node) + if node.module == '__future__': + importation.used = (self.scope, node) + self.addBinding(node, importation) diff --git a/sublimelinter/modules/libs/pyflakes/messages.py b/sublimelinter/modules/libs/pyflakes/messages.py new file mode 100644 index 00000000..7be1a52c --- /dev/null +++ b/sublimelinter/modules/libs/pyflakes/messages.py @@ -0,0 +1,128 @@ +# (c) 2005 Divmod, Inc. See LICENSE file for details + + +class Message(object): + message = '' + + def __init__(self, filename, loc, use_column=True, level='W', message_args=()): + self.filename = filename + self.lineno = loc.lineno + self.col = getattr(loc, 'col_offset', None) if use_column else None + self.level = level + self.message_args = message_args + + def __str__(self): + if self.col is not None: + return '%s:%s(%d): [%s] %s' % (self.filename, self.lineno, self.col, self.level, self.message % self.message_args) + else: + return '%s:%s: [%s] %s' % (self.filename, self.lineno, self.level, self.message % self.message_args) + + +class UnusedImport(Message): + message = '%r imported but unused' + + def __init__(self, filename, loc, name): + Message.__init__(self, filename, loc, use_column=False, message_args=(name,)) + self.name = name + + +class RedefinedWhileUnused(Message): + message = 'redefinition of unused %r from line %r' + + def __init__(self, filename, loc, name, orig_loc): + Message.__init__(self, filename, loc, message_args=(name, orig_loc.lineno)) + self.name = name + self.orig_loc = orig_loc + + +class ImportShadowedByLoopVar(Message): + message = 'import %r from line %r shadowed by loop variable' + + def __init__(self, filename, loc, name, orig_loc): + Message.__init__(self, filename, loc, message_args=(name, orig_loc.lineno)) + self.name = name + self.orig_loc = orig_loc + + +class ImportStarUsed(Message): + message = "'from %s import *' used; unable to detect undefined names" + + def __init__(self, filename, loc, modname): + Message.__init__(self, filename, loc, message_args=(modname,)) + self.name = modname + + +class UndefinedName(Message): + message = 'undefined name %r' + + def __init__(self, filename, loc, name): + Message.__init__(self, filename, loc, level='E', message_args=(name,)) + self.name = name + + +class UndefinedExport(Message): + message = 'undefined name %r in __all__' + + def __init__(self, filename, loc, name): + Message.__init__(self, filename, loc, level='E', message_args=(name,)) + self.name = name + + +class UndefinedLocal(Message): + message = "local variable %r (defined in enclosing scope on line %r) referenced before assignment" + + def __init__(self, filename, loc, name, orig_loc): + Message.__init__(self, filename, loc, level='E', message_args=(name, orig_loc.lineno)) + self.name = name + self.orig_loc = orig_loc + + +class DuplicateArgument(Message): + message = 'duplicate argument %r in function definition' + + def __init__(self, filename, loc, name): + Message.__init__(self, filename, loc, level='E', message_args=(name,)) + self.name = name + + +class RedefinedFunction(Message): + message = 'redefinition of function %r from line %r' + + def __init__(self, filename, loc, name, orig_loc): + Message.__init__(self, filename, loc, message_args=(name, orig_loc.lineno)) + self.name = name + self.orig_loc = orig_loc + + +class CouldNotCompile(Message): + def __init__(self, filename, loc, msg=None, line=None): + if msg and line: + self.message = 'could not compile: %s\n%s' + message_args = (msg, line) + else: + self.message = 'could not compile' + message_args = () + Message.__init__(self, filename, loc, level='E', message_args=message_args) + self.msg = msg + self.line = line + + +class LateFutureImport(Message): + message = 'future import(s) %r after other statements' + + def __init__(self, filename, loc, names): + Message.__init__(self, filename, loc, message_args=(names,)) + self.names = names + + +class UnusedVariable(Message): + """ + Indicates that a variable has been explicity assigned to but not actually + used. + """ + + message = 'local variable %r is assigned to but never used' + + def __init__(self, filename, loc, name): + Message.__init__(self, filename, loc, message_args=(name,)) + self.name = name diff --git a/sublimelinter/modules/notes.py b/sublimelinter/modules/notes.py new file mode 100644 index 00000000..2b1a5c66 --- /dev/null +++ b/sublimelinter/modules/notes.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +'''notes.py + +Used to highlight user-defined "annotations" such as TODO, README, etc., +depending user choice. +''' + +import sublime + +from base_linter import BaseLinter + +CONFIG = { + 'language': 'annotations' +} + + +class Linter(BaseLinter): + DEFAULT_NOTES = ["TODO", "README", "FIXME"] + + def built_in_check(self, view, code, filename): + annotations = self.select_annotations(view) + regions = [] + + for annotation in annotations: + regions.extend(self.find_all(code, annotation, view)) + + return regions + + def select_annotations(self, view): + '''selects the list of annotations to use''' + annotations = view.settings().get("annotations") + + if annotations is None: + return self.DEFAULT_NOTES + else: + return annotations + + def extract_annotations(self, code, view, filename): + '''extract all lines with annotations''' + annotations = self.select_annotations(view) + annotation_starts = [] + + for annotation in annotations: + start = 0 + length = len(annotation) + + while True: + start = code.find(annotation, start) + + if start != -1: + end = start + length + annotation_starts.append(start) + start = end + else: + break + + regions_with_notes = set([]) + + for point in annotation_starts: + regions_with_notes.add(view.extract_scope(point)) + + regions_with_notes = sorted(list(regions_with_notes)) + text = [] + + for region in regions_with_notes: + row, col = view.rowcol(region.begin()) + text.append("[{0}:{1}]".format(filename, row + 1)) + text.append(view.substr(region)) + + return '\n'.join(text) + + def find_all(self, text, string, view): + ''' finds all occurences of "string" in "text" and notes their positions + as a sublime Region + ''' + found = [] + length = len(string) + start = 0 + + while True: + start = text.find(string, start) + + if start != -1: + end = start + length + found.append(sublime.Region(start, end)) + start = end + else: + break + + return found diff --git a/sublimelinter/modules/objective-j.py b/sublimelinter/modules/objective-j.py new file mode 100644 index 00000000..35f0e0e7 --- /dev/null +++ b/sublimelinter/modules/objective-j.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# objective-j.py - Lint checking for Objective-J - given filename and contents of the code: +# It provides a list of line numbers to outline and offsets to highlight. +# +# This specific module is part of the SublimeLinter project. +# It is a fork of the original SublimeLint project, +# (c) 2011 Ryan Hileman and licensed under the MIT license. +# URL: http://bochs.info/ +# +# The original copyright notices for this file/project follows: +# +# (c) 2005-2008 Divmod, Inc. +# See LICENSE file for details +# +# The LICENSE file is as follows: +# +# Copyright (c) 2005 Divmod, Inc., http://www.divmod.com/ +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +from capp_lint import LintChecker +from base_linter import BaseLinter + +CONFIG = { + 'language': 'Objective-J' +} + + +class Linter(BaseLinter): + def built_in_check(self, view, code, filename): + checker = LintChecker(view) + checker.lint_text(code, filename) + return checker.errors + + def parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages): + for error in errors: + lineno = error['lineNum'] + self.add_message(lineno, lines, error['message'], errorMessages if type == LintChecker.ERROR_TYPE_ILLEGAL else warningMessages) + + for position in error.get('positions', []): + self.underline_range(view, lineno, position, errorUnderlines if type == LintChecker.ERROR_TYPE_ILLEGAL else warningUnderlines) diff --git a/sublimelinter/modules/perl.py b/sublimelinter/modules/perl.py new file mode 100644 index 00000000..732832b2 --- /dev/null +++ b/sublimelinter/modules/perl.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# perl.py - sublimelint package for checking perl files + +import re + +from base_linter import BaseLinter + +CONFIG = { + 'language': 'perl', + 'executable': 'perl', + 'lint_args': '-c' +} + + +class Linter(BaseLinter): + def parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages): + for line in errors.splitlines(): + match = re.match(r'(?P.+?) at .+? line (?P\d+)(, near "(?P.+?)")?', line) + + if match: + error, line = match.group('error'), match.group('line') + lineno = int(line) + near = match.group('near') + + if near: + error = '{0}, near "{1}"'.format(error, near) + self.underline_regex(view, lineno, '(?P{0})'.format(re.escape(near)), lines, errorUnderlines) + + self.add_message(lineno, lines, error, errorMessages) diff --git a/sublimelinter/modules/php.py b/sublimelinter/modules/php.py new file mode 100644 index 00000000..546df4dc --- /dev/null +++ b/sublimelinter/modules/php.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# php.py - sublimelint package for checking php files + +import re + +from base_linter import BaseLinter + +CONFIG = { + 'language': 'php', + 'executable': 'php', + 'lint_args': ('-l', '-d display_errors=On') +} + + +class Linter(BaseLinter): + def parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages): + for line in errors.splitlines(): + match = re.match(r'^Parse error:\s*(?:\w+ error,\s*)?(?P.+?)\s+in\s+.+?\s*line\s+(?P\d+)', line) + + if match: + error, line = match.group('error'), match.group('line') + self.add_message(int(line), lines, error, errorMessages) diff --git a/sublimelinter/modules/python.py b/sublimelinter/modules/python.py new file mode 100644 index 00000000..53fe9bd6 --- /dev/null +++ b/sublimelinter/modules/python.py @@ -0,0 +1,262 @@ +# -*- coding: utf-8 -*- +# python.py - Lint checking for Python - given filename and contents of the code: +# It provides a list of line numbers to outline and offsets to highlight. +# +# This specific module is part of the SublimeLinter project. +# It is a fork by André Roberge from the original SublimeLint project, +# (c) 2011 Ryan Hileman and licensed under the MIT license. +# URL: http://bochs.info/ +# +# The original copyright notices for this file/project follows: +# +# (c) 2005-2008 Divmod, Inc. +# See LICENSE file for details +# +# The LICENSE file is as follows: +# +# Copyright (c) 2005 Divmod, Inc., http://www.divmod.com/ +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +# TODO: +# * fix regex for variable names inside strings (quotes) + +import re +import _ast + +import pep8 +import pyflakes.checker as pyflakes + +from base_linter import BaseLinter + +pyflakes.messages.Message.__str__ = lambda self: self.message % self.message_args + +CONFIG = { + 'language': 'python' +} + + +class Pep8Error(pyflakes.messages.Message): + message = 'PEP 8 (%s): %s' + + def __init__(self, filename, loc, code, text): + # PEP 8 Errors are downgraded to "warnings" + pyflakes.messages.Message.__init__(self, filename, loc, level='W', message_args=(code, text)) + self.text = text + + +class Pep8Warning(pyflakes.messages.Message): + message = 'PEP 8 (%s): %s' + + def __init__(self, filename, loc, code, text): + # PEP 8 Warnings are downgraded to "violations" + pyflakes.messages.Message.__init__(self, filename, loc, level='V', message_args=(code, text)) + self.text = text + + +class OffsetError(pyflakes.messages.Message): + message = '%r at column %r' + + def __init__(self, filename, loc, text, offset): + pyflakes.messages.Message.__init__(self, filename, loc, level='E', message_args=(text, offset + 1)) + self.text = text + self.offset = offset + + +class PythonError(pyflakes.messages.Message): + message = '%r' + + def __init__(self, filename, loc, text): + pyflakes.messages.Message.__init__(self, filename, loc, level='E', message_args=(text,)) + self.text = text + + +class Dict2Obj: + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + +class Linter(BaseLinter): + def pyflakes_check(self, code, filename, ignore=None): + try: + tree = compile(code, filename, "exec", _ast.PyCF_ONLY_AST) + except (SyntaxError, IndentationError), value: + msg = value.args[0] + + (lineno, offset, text) = value.lineno, value.offset, value.text + + # If there's an encoding problem with the file, the text is None. + if text is None: + # Avoid using msg, since for the only known case, it contains a + # bogus message that claims the encoding the file declared was + # unknown. + if msg.startswith('duplicate argument'): + arg = msg.split('duplicate argument ', 1)[1].split(' ', 1)[0].strip('\'"') + error = pyflakes.messages.DuplicateArgument(filename, value, arg) + else: + error = PythonError(filename, value, msg) + else: + line = text.splitlines()[-1] + + if offset is not None: + offset = offset - (len(text) - len(line)) + + if offset is not None: + error = OffsetError(filename, value, msg, offset) + else: + error = PythonError(filename, value, msg) + return [error] + except ValueError, e: + return [PythonError(filename, 0, e.args[0])] + else: + # Okay, it's syntactically valid. Now check it. + if ignore is not None: + old_magic_globals = pyflakes._MAGIC_GLOBALS + pyflakes._MAGIC_GLOBALS += ignore + w = pyflakes.Checker(tree, filename) + if ignore is not None: + pyflakes._MAGIC_GLOBALS = old_magic_globals + return w.messages + + def pep8_check(self, code, filename, ignore=None): + messages = [] + _lines = code.split('\n') + + if _lines: + def report_error(self, line_number, offset, text, check): + code = text[:4] + msg = text[5:] + + if pep8.ignore_code(code): + return + elif code.startswith('E'): + messages.append(Pep8Error(filename, Dict2Obj(lineno=line_number, col_offset=offset), code, msg)) + else: + messages.append(Pep8Warning(filename, Dict2Obj(lineno=line_number, col_offset=offset), code, msg)) + + pep8.Checker.report_error = report_error + _ignore = ignore + pep8.DEFAULT_IGNORE.split(',') + + class FakeOptions: + verbose = 0 + select = [] + ignore = _ignore + + pep8.options = FakeOptions() + pep8.options.physical_checks = pep8.find_checks('physical_line') + pep8.options.logical_checks = pep8.find_checks('logical_line') + pep8.options.counters = dict.fromkeys(pep8.BENCHMARK_KEYS, 0) + good_lines = [l + '\n' for l in _lines] + good_lines[-1] = good_lines[-1].rstrip('\n') + + if not good_lines[-1]: + good_lines = good_lines[:-1] + + try: + pep8.Checker(filename, good_lines).check_all() + except: + pass + + return messages + + def built_in_check(self, view, code, filename): + errors = [] + + if view.settings().get("pep8", True): + errors.extend(self.pep8_check(code, filename, ignore=view.settings().get('pep8_ignore', []))) + + pyflakes_ignore = view.settings().get('pyflakes_ignore', None) + pyflakes_disabled = view.settings().get('pyflakes_disabled', False) + + if not pyflakes_disabled: + errors.extend(self.pyflakes_check(code, filename, pyflakes_ignore)) + + return errors + + def parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages): + + def underline_word(lineno, word, underlines): + regex = r'((and|or|not|if|elif|while|in)\s+|[+\-*^%%<>=\(\{{])*\s*(?P[\w\.]*{0}[\w]*)'.format(re.escape(word)) + self.underline_regex(view, lineno, regex, lines, underlines, word) + + def underline_import(lineno, word, underlines): + linematch = '(from\s+[\w_\.]+\s+)?import\s+(?P[^#;]+)' + regex = '(^|\s+|,\s*|as\s+)(?P[\w]*{0}[\w]*)'.format(re.escape(word)) + self.underline_regex(view, lineno, regex, lines, underlines, word, linematch) + + def underline_for_var(lineno, word, underlines): + regex = 'for\s+(?P[\w]*{0}[\w*])'.format(re.escape(word)) + self.underline_regex(view, lineno, regex, lines, underlines, word) + + def underline_duplicate_argument(lineno, word, underlines): + regex = 'def [\w_]+\(.*?(?P[\w]*{0}[\w]*)'.format(re.escape(word)) + self.underline_regex(view, lineno, regex, lines, underlines, word) + + errors.sort(lambda a, b: cmp(a.lineno, b.lineno)) + ignoreImportStar = view.settings().get('pyflakes_ignore_import_*', True) + + for error in errors: + if error.level == 'E': + messages = errorMessages + underlines = errorUnderlines + elif error.level == 'V': + messages = violationMessages + underlines = violationUnderlines + elif error.level == 'W': + messages = warningMessages + underlines = warningUnderlines + + if isinstance(error, pyflakes.messages.ImportStarUsed) and ignoreImportStar: + continue + + self.add_message(error.lineno, lines, str(error), messages) + + if isinstance(error, (Pep8Error, Pep8Warning)): + self.underline_range(view, error.lineno, error.col, underlines) + + elif isinstance(error, OffsetError): + self.underline_range(view, error.lineno, error.offset, underlines) + + elif isinstance(error, (pyflakes.messages.RedefinedWhileUnused, + pyflakes.messages.UndefinedName, + pyflakes.messages.UndefinedExport, + pyflakes.messages.UndefinedLocal, + pyflakes.messages.RedefinedFunction, + pyflakes.messages.UnusedVariable)): + underline_word(error.lineno, error.name, underlines) + + elif isinstance(error, pyflakes.messages.ImportShadowedByLoopVar): + underline_for_var(error.lineno, error.name, underlines) + + elif isinstance(error, pyflakes.messages.UnusedImport): + underline_import(error.lineno, error.name, underlines) + + elif isinstance(error, pyflakes.messages.ImportStarUsed): + underline_import(error.lineno, '*', underlines) + + elif isinstance(error, pyflakes.messages.DuplicateArgument): + underline_duplicate_argument(error.lineno, error.name, underlines) + + elif isinstance(error, pyflakes.messages.LateFutureImport): + pass + + else: + print 'Oops, we missed an error type!', type(error) diff --git a/sublimelinter/modules/ruby.py b/sublimelinter/modules/ruby.py new file mode 100644 index 00000000..adf7bed4 --- /dev/null +++ b/sublimelinter/modules/ruby.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# ruby.py - sublimelint package for checking ruby files + +import re + +from base_linter import BaseLinter + +CONFIG = { + 'language': 'ruby', + 'executable': 'ruby', + 'lint_args': '-wc' +} + + +class Linter(BaseLinter): + def parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages): + for line in errors.splitlines(): + match = re.match(r'^.+:(?P\d+):\s+(?P.+)', line) + + if match: + error, line = match.group('error'), match.group('line') + self.add_message(int(line), lines, error, errorMessages) diff --git a/sublimelinter/modules/sublime_pylint.py b/sublimelinter/modules/sublime_pylint.py new file mode 100644 index 00000000..00702d7c --- /dev/null +++ b/sublimelinter/modules/sublime_pylint.py @@ -0,0 +1,83 @@ +''' sublime_pylint.py - sublimelint package for checking python files + +pylint is not available as a checker that runs in the background +as it generally takes much too long. +''' + +from StringIO import StringIO +import tempfile + +try: + from pylint import checkers + from pylint import lint + PYLINT_AVAILABLE = True +except ImportError: + PYLINT_AVAILABLE = False + +from base_linter import BaseLinter + +CONFIG = { + 'language': 'pylint' +} + + +class Linter(BaseLinter): + def get_executable(self, view): + return (PYLINT_AVAILABLE, None, 'built in' if PYLINT_AVAILABLE else 'the pylint module could not be imported') + + def built_in_check(self, view, code, filename): + linter = lint.PyLinter() + checkers.initialize(linter) + + # Disable some errors. + linter.load_command_line_configuration([ + '--module-rgx=.*', # don't check the module name + '--reports=n', # remove tables + '--persistent=n', # don't save the old score (no sense for temp) + ]) + + temp = tempfile.NamedTemporaryFile(suffix='.py') + temp.write(code) + temp.flush() + + output_buffer = StringIO() + linter.reporter.set_output(output_buffer) + linter.check(temp.name) + report = output_buffer.getvalue().replace(temp.name, 'line ') + + output_buffer.close() + temp.close() + + return report + + def remove_unwanted(self, errors): + '''remove unwanted warnings''' + ## todo: investigate how this can be set by a user preference + # as it appears that the user pylint configuration file is ignored. + lines = errors.split('\n') + wanted = [] + unwanted = ["Found indentation with tabs instead of spaces", + "************* Module"] + + for line in lines: + for not_include in unwanted: + if not_include in line: + break + else: + wanted.append(line) + + return '\n'.join(wanted) + + def parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages): + errors = self.remove_unwanted(errors) + + for line in errors.splitlines(): + info = line.split(":") + + try: + lineno = info[1] + except IndexError: + print info + + message = ":".join(info[2:]) + self.add_message(int(lineno), lines, message, errorMessages)