|
| 1 | +# Copyright 2017 Palantir Technologies, Inc. |
| 2 | +import logging |
| 3 | +import re |
| 4 | +import sys |
| 5 | +import importmagic |
| 6 | +from pyls import hookimpl, lsp, _utils |
| 7 | + |
| 8 | + |
| 9 | +log = logging.getLogger(__name__) |
| 10 | + |
| 11 | +SOURCE = 'importmagic' |
| 12 | +ADD_IMPORT_COMMAND = 'importmagic.addimport' |
| 13 | +MAX_COMMANDS = 4 |
| 14 | +UNRES_RE = re.compile(r"Unresolved import '(?P<unresolved>[\w.]+)'") |
| 15 | + |
| 16 | +_index_cache = {} |
| 17 | + |
| 18 | + |
| 19 | +def _get_index(sys_path): |
| 20 | + """Build index of symbols from python modules. |
| 21 | + Cache the index so we don't build it multiple times unnecessarily. |
| 22 | + """ |
| 23 | + key = tuple(sys_path) |
| 24 | + if key not in _index_cache: |
| 25 | + log.debug("Started building importmagic index") |
| 26 | + index = importmagic.SymbolIndex() |
| 27 | + # The build tend to be noisy |
| 28 | + logging.getLogger('importmagic.index').setLevel(logging.ERROR) |
| 29 | + index.build_index(paths=sys_path) |
| 30 | + _index_cache[key] = index |
| 31 | + logging.getLogger('importmagic.index').setLevel(logging.DEBUG) |
| 32 | + log.debug("Finished building importmagic index") |
| 33 | + return _index_cache[key] |
| 34 | + |
| 35 | + |
| 36 | +@hookimpl |
| 37 | +def pyls_commands(): |
| 38 | + return [ADD_IMPORT_COMMAND] |
| 39 | + |
| 40 | + |
| 41 | +@hookimpl |
| 42 | +def pyls_lint(document): |
| 43 | + """Build a diagnostics of unresolved symbols. Every entry follows this format: |
| 44 | + { |
| 45 | + 'source': 'importmagic', |
| 46 | + 'range': { |
| 47 | + 'start': { |
| 48 | + 'line': start_line, |
| 49 | + 'character': start_column, |
| 50 | + }, |
| 51 | + 'end': { |
| 52 | + 'line': end_line, |
| 53 | + 'character': end_column, |
| 54 | + }, |
| 55 | + }, |
| 56 | + 'message': 'Unresolved import <symbol>', |
| 57 | + 'severity': lsp.DiagnosticSeverity.Hint, |
| 58 | + } |
| 59 | +
|
| 60 | + Args: |
| 61 | + document: The document to be linted. |
| 62 | + Returns: |
| 63 | + A list of dictionaries. |
| 64 | + """ |
| 65 | + scope = importmagic.Scope.from_source(document.source) |
| 66 | + unresolved, _unreferenced = scope.find_unresolved_and_unreferenced_symbols() |
| 67 | + |
| 68 | + diagnostics = [] |
| 69 | + |
| 70 | + # Annoyingly, we only get the text of an unresolved import, so we'll look for it ourselves |
| 71 | + for unres in unresolved: |
| 72 | + if unres not in document.source: |
| 73 | + continue |
| 74 | + |
| 75 | + for line_no, line in enumerate(document.lines): |
| 76 | + pos = line.find(unres) |
| 77 | + if pos < 0: |
| 78 | + continue |
| 79 | + |
| 80 | + diagnostics.append({ |
| 81 | + 'source': SOURCE, |
| 82 | + 'range': { |
| 83 | + 'start': {'line': line_no, 'character': pos}, |
| 84 | + 'end': {'line': line_no, 'character': pos + len(unres)} |
| 85 | + }, |
| 86 | + 'message': "Unresolved import '%s'" % unres, |
| 87 | + 'severity': lsp.DiagnosticSeverity.Hint, |
| 88 | + }) |
| 89 | + |
| 90 | + return diagnostics |
| 91 | + |
| 92 | + |
| 93 | +@hookimpl |
| 94 | +def pyls_code_actions(config, document, context): |
| 95 | + """Build a list of actions to be suggested to the user. Each action follow this format: |
| 96 | + { |
| 97 | + 'title': 'importmagic', |
| 98 | + 'command': command ('importmagic.add_import'), |
| 99 | + 'arguments': |
| 100 | + { |
| 101 | + 'uri': document.uri, |
| 102 | + 'version': document.version, |
| 103 | + 'startLine': start_line, |
| 104 | + 'endLine': end_line, |
| 105 | + 'newText': text, |
| 106 | + } |
| 107 | + } |
| 108 | + """ |
| 109 | + # Update the style configuration |
| 110 | + conf = config.plugin_settings('importmagic_lint') |
| 111 | + log.debug("Got importmagic settings: %s", conf) |
| 112 | + importmagic.Imports.set_style(**{_utils.camel_to_underscore(k): v for k, v in conf.items()}) |
| 113 | + |
| 114 | + actions = [] |
| 115 | + diagnostics = context.get('diagnostics', []) |
| 116 | + for diagnostic in diagnostics: |
| 117 | + if diagnostic.get('source') != SOURCE: |
| 118 | + continue |
| 119 | + m = UNRES_RE.match(diagnostic['message']) |
| 120 | + if not m: |
| 121 | + continue |
| 122 | + |
| 123 | + unres = m.group('unresolved') |
| 124 | + # Might be slow but is cached once built |
| 125 | + index = _get_index(sys.path) |
| 126 | + |
| 127 | + for score, module, variable in sorted(index.symbol_scores(unres)[:MAX_COMMANDS], reverse=True): |
| 128 | + if score < 1: |
| 129 | + # These tend to be terrible |
| 130 | + continue |
| 131 | + |
| 132 | + # Generate the patch we would need to apply |
| 133 | + imports = importmagic.Imports(index, document.source) |
| 134 | + if variable: |
| 135 | + imports.add_import_from(module, variable) |
| 136 | + else: |
| 137 | + imports.add_import(module) |
| 138 | + start_line, end_line, text = imports.get_update() |
| 139 | + |
| 140 | + actions.append({ |
| 141 | + 'title': _command_title(variable, module), |
| 142 | + 'command': ADD_IMPORT_COMMAND, |
| 143 | + 'arguments': [{ |
| 144 | + 'uri': document.uri, |
| 145 | + 'version': document.version, |
| 146 | + 'startLine': start_line, |
| 147 | + 'endLine': end_line, |
| 148 | + 'newText': text |
| 149 | + }] |
| 150 | + }) |
| 151 | + return actions |
| 152 | + |
| 153 | + |
| 154 | +@hookimpl |
| 155 | +def pyls_execute_command(workspace, command, arguments): |
| 156 | + if command != ADD_IMPORT_COMMAND: |
| 157 | + return |
| 158 | + |
| 159 | + args = arguments[0] |
| 160 | + |
| 161 | + edit = {'documentChanges': [{ |
| 162 | + 'textDocument': { |
| 163 | + 'uri': args['uri'], |
| 164 | + 'version': args['version'] |
| 165 | + }, |
| 166 | + 'edits': [{ |
| 167 | + 'range': { |
| 168 | + 'start': {'line': args['startLine'], 'character': 0}, |
| 169 | + 'end': {'line': args['endLine'], 'character': 0}, |
| 170 | + }, |
| 171 | + 'newText': args['newText'] |
| 172 | + }] |
| 173 | + }]} |
| 174 | + workspace.apply_edit(edit) |
| 175 | + |
| 176 | + |
| 177 | +def _command_title(variable, module): |
| 178 | + if not variable: |
| 179 | + return 'Import "%s"' % module |
| 180 | + return 'Import "%s" from "%s"' % (variable, module) |
0 commit comments