22import logging
33import re
44import sys
5+ import tokenize
6+ from io import BytesIO
57from concurrent .futures import ThreadPoolExecutor
68import importmagic
79from pyls import hookimpl , lsp , _utils
@@ -62,6 +64,59 @@ def _get_imports_list(source, index=None):
6264 return imported
6365
6466
67+ def _tokenize (source ):
68+ """Tokenize python source code.
69+ """
70+ stream = BytesIO (source .encode ())
71+ return list (tokenize .tokenize (stream .readline ))
72+
73+
74+ def _search_symbol (source , symbol ):
75+ """Search symbol in python source code.
76+
77+ Args:
78+ source: str object of the source code
79+ symbol: str object of the symbol to search
80+
81+ Returns:
82+ list of locations where the symbol was found. Each element have the following format
83+ {
84+ 'start': {
85+ 'line': int,
86+ 'character': int
87+ },
88+ 'end': {
89+ 'line': int,
90+ 'character': int
91+ }
92+ }
93+ """
94+ symbol_tokens = _tokenize (symbol )
95+ source_tokens = _tokenize (source )
96+
97+ get_str = lambda token : token [1 ]
98+ symbol_tokens_str = list (map (get_str , symbol_tokens ))
99+ source_tokens_str = list (map (get_str , source_tokens ))
100+
101+ symbol_len = len (symbol_tokens )
102+ locations = []
103+ for i in len (source_tokens ):
104+ if source_tokens_str [i :i + symbol_len ] == symbol_tokens_str :
105+ location_range = {
106+ 'start' : {
107+ 'line' : source_tokens [2 ][0 ] - 1 ,
108+ 'character' : source_tokens [2 ][1 ],
109+ },
110+ 'end' : {
111+ 'line' : source_tokens [3 ][0 ] - 1 ,
112+ 'character' : source_tokens [3 ][1 ],
113+ }
114+ }
115+ locations .append (location_range )
116+
117+ return locations
118+
119+
65120@hookimpl
66121def pyls_initialize ():
67122 _index_cache ['default' ] = None
@@ -107,27 +162,16 @@ def pyls_lint(document):
107162
108163 # Annoyingly, we only get the text of an unresolved import, so we'll look for it ourselves
109164 for unres in unresolved :
110- for line_no , line in enumerate (document .lines ):
111- pos = line .find (unres )
112- if pos < 0 :
113- continue
114-
165+ for location_range in _search_symbol (document .source , unres ):
115166 diagnostics .append ({
116167 'source' : SOURCE ,
117- 'range' : {
118- 'start' : {'line' : line_no , 'character' : pos },
119- 'end' : {'line' : line_no , 'character' : pos + len (unres )}
120- },
168+ 'range' : location_range ,
121169 'message' : "Unresolved import '%s'" % unres ,
122170 'severity' : lsp .DiagnosticSeverity .Hint ,
123171 })
124172
125173 for unref in unreferenced :
126- for line_no , line in enumerate (document .lines ):
127- pos = line .find (unref )
128- if pos < 0 :
129- continue
130-
174+ for location_range in _search_symbol (document .source , unref ):
131175 # Find out if the unref is an import or a variable/func
132176 imports = _get_imports_list (document .source )
133177 if unref in imports :
@@ -137,10 +181,7 @@ def pyls_lint(document):
137181
138182 diagnostics .append ({
139183 'source' : SOURCE ,
140- 'range' : {
141- 'start' : {'line' : line_no , 'character' : pos },
142- 'end' : {'line' : line_no , 'character' : pos + len (unref )}
143- },
184+ 'range' : location_range ,
144185 'message' : message ,
145186 'severity' : lsp .DiagnosticSeverity .Warning ,
146187 })
0 commit comments