skylos 1.2.2__py3-none-any.whl → 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of skylos might be problematic. Click here for more details.
- skylos/__init__.py +1 -1
- skylos/analyzer.py +103 -121
- skylos/cli.py +66 -109
- skylos/codemods.py +89 -0
- skylos/constants.py +25 -10
- skylos/framework_aware.py +290 -90
- skylos/server.py +560 -0
- skylos/test_aware.py +0 -1
- skylos/visitor.py +249 -90
- {skylos-1.2.2.dist-info → skylos-2.1.0.dist-info}/METADATA +4 -1
- {skylos-1.2.2.dist-info → skylos-2.1.0.dist-info}/RECORD +16 -13
- test/test_codemods.py +153 -0
- test/test_framework_aware.py +176 -242
- {skylos-1.2.2.dist-info → skylos-2.1.0.dist-info}/WHEEL +0 -0
- {skylos-1.2.2.dist-info → skylos-2.1.0.dist-info}/entry_points.txt +0 -0
- {skylos-1.2.2.dist-info → skylos-2.1.0.dist-info}/top_level.txt +0 -0
skylos/__init__.py
CHANGED
skylos/analyzer.py
CHANGED
|
@@ -6,61 +6,29 @@ import logging
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from collections import defaultdict
|
|
8
8
|
from skylos.visitor import Visitor
|
|
9
|
-
from skylos.constants import (
|
|
9
|
+
from skylos.constants import ( PENALTIES, AUTO_CALLED )
|
|
10
10
|
from skylos.test_aware import TestAwareVisitor
|
|
11
11
|
import os
|
|
12
12
|
import traceback
|
|
13
13
|
from skylos.framework_aware import FrameworkAwareVisitor, detect_framework_usage
|
|
14
|
-
import io
|
|
15
|
-
import tokenize
|
|
16
|
-
import re
|
|
17
|
-
import warnings
|
|
18
14
|
|
|
19
15
|
logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(levelname)s - %(message)s')
|
|
20
16
|
logger=logging.getLogger('Skylos')
|
|
21
17
|
|
|
22
|
-
def parse_exclude_folders(user_exclude_folders, use_defaults=True, include_folders=None):
|
|
23
|
-
exclude_set = set()
|
|
24
|
-
|
|
25
|
-
if use_defaults:
|
|
26
|
-
exclude_set.update(DEFAULT_EXCLUDE_FOLDERS)
|
|
27
|
-
|
|
28
|
-
if user_exclude_folders:
|
|
29
|
-
exclude_set.update(user_exclude_folders)
|
|
30
|
-
|
|
31
|
-
if include_folders:
|
|
32
|
-
for folder in include_folders:
|
|
33
|
-
exclude_set.discard(folder)
|
|
34
|
-
|
|
35
|
-
return exclude_set
|
|
36
|
-
|
|
37
|
-
IGNORE_PATTERNS = (
|
|
38
|
-
r"#\s*pragma:\s*no\s+skylos", ## our own pragma
|
|
39
|
-
r"#\s*pragma:\s*no\s+cover",
|
|
40
|
-
r"#\s*noqa(?:\b|:)", # flake8 style
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
def _collect_ignored_lines(source: str) -> set[int]:
|
|
44
|
-
ignores = set()
|
|
45
|
-
for tok in tokenize.generate_tokens(io.StringIO(source).readline):
|
|
46
|
-
if tok.type == tokenize.COMMENT:
|
|
47
|
-
if any(re.search(pat, tok.string, flags=re.I) for pat in IGNORE_PATTERNS):
|
|
48
|
-
ignores.add(tok.start[0])
|
|
49
|
-
return ignores
|
|
50
|
-
|
|
51
18
|
class Skylos:
|
|
52
19
|
def __init__(self):
|
|
53
20
|
self.defs={}
|
|
54
21
|
self.refs=[]
|
|
55
22
|
self.dynamic=set()
|
|
56
23
|
self.exports=defaultdict(set)
|
|
57
|
-
self.ignored_lines:set[int]=set()
|
|
58
24
|
|
|
59
25
|
def _module(self,root,f):
|
|
60
|
-
p=list(f.relative_to(root).parts)
|
|
61
|
-
if p[-1].endswith(".py"):
|
|
62
|
-
|
|
63
|
-
|
|
26
|
+
p = list(f.relative_to(root).parts)
|
|
27
|
+
if p[-1].endswith(".py"):
|
|
28
|
+
p[-1] = p[-1][:-3]
|
|
29
|
+
if p[-1] == "__init__":
|
|
30
|
+
p.pop()
|
|
31
|
+
return ".".join(p)
|
|
64
32
|
|
|
65
33
|
def _should_exclude_file(self, file_path, root_path, exclude_folders):
|
|
66
34
|
if not exclude_folders:
|
|
@@ -111,9 +79,9 @@ class Skylos:
|
|
|
111
79
|
return all_files, root
|
|
112
80
|
|
|
113
81
|
def _mark_exports(self):
|
|
114
|
-
for name,
|
|
115
|
-
if
|
|
116
|
-
|
|
82
|
+
for name, definition in self.defs.items():
|
|
83
|
+
if definition.in_init and not definition.simple_name.startswith('_'):
|
|
84
|
+
definition.is_exported = True
|
|
117
85
|
|
|
118
86
|
for mod, export_names in self.exports.items():
|
|
119
87
|
for name in export_names:
|
|
@@ -137,23 +105,37 @@ class Skylos:
|
|
|
137
105
|
break
|
|
138
106
|
|
|
139
107
|
simple_name_lookup = defaultdict(list)
|
|
140
|
-
for
|
|
141
|
-
simple_name_lookup[
|
|
108
|
+
for definition in self.defs.values():
|
|
109
|
+
simple_name_lookup[definition.simple_name].append(definition)
|
|
142
110
|
|
|
143
111
|
for ref, _ in self.refs:
|
|
144
112
|
if ref in self.defs:
|
|
145
113
|
self.defs[ref].references += 1
|
|
146
|
-
|
|
147
114
|
if ref in import_to_original:
|
|
148
115
|
original = import_to_original[ref]
|
|
149
116
|
self.defs[original].references += 1
|
|
150
117
|
continue
|
|
151
|
-
|
|
118
|
+
|
|
152
119
|
simple = ref.split('.')[-1]
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
120
|
+
ref_mod = ref.rsplit(".", 1)[0]
|
|
121
|
+
candidates = simple_name_lookup.get(simple, [])
|
|
122
|
+
|
|
123
|
+
if ref_mod:
|
|
124
|
+
filtered = []
|
|
125
|
+
for d in candidates:
|
|
126
|
+
if d.name.startswith(ref_mod + ".") and d.type != "import":
|
|
127
|
+
filtered.append(d)
|
|
128
|
+
candidates = filtered
|
|
129
|
+
else:
|
|
130
|
+
filtered = []
|
|
131
|
+
for d in candidates:
|
|
132
|
+
if d.type != "import":
|
|
133
|
+
filtered.append(d)
|
|
134
|
+
candidates = filtered
|
|
135
|
+
|
|
136
|
+
if len(candidates) == 1:
|
|
137
|
+
candidates[0].references += 1
|
|
138
|
+
|
|
157
139
|
for module_name in self.dynamic:
|
|
158
140
|
for def_name, def_obj in self.defs.items():
|
|
159
141
|
if def_obj.name.startswith(f"{module_name}."):
|
|
@@ -172,59 +154,66 @@ class Skylos:
|
|
|
172
154
|
return []
|
|
173
155
|
|
|
174
156
|
def _apply_penalties(self, def_obj, visitor, framework):
|
|
175
|
-
|
|
176
|
-
|
|
157
|
+
confidence=100
|
|
177
158
|
if def_obj.simple_name.startswith("_") and not def_obj.simple_name.startswith("__"):
|
|
178
|
-
|
|
159
|
+
confidence -= PENALTIES["private_name"]
|
|
179
160
|
if def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__"):
|
|
180
|
-
|
|
181
|
-
if def_obj.type == "variable" and def_obj.simple_name
|
|
182
|
-
|
|
161
|
+
confidence -= PENALTIES["dunder_or_magic"]
|
|
162
|
+
if def_obj.type == "variable" and def_obj.simple_name.isupper():
|
|
163
|
+
confidence = 0
|
|
183
164
|
if def_obj.in_init and def_obj.type in ("function", "class"):
|
|
184
|
-
|
|
165
|
+
confidence -= PENALTIES["in_init_file"]
|
|
185
166
|
if def_obj.name.split(".")[0] in self.dynamic:
|
|
186
|
-
|
|
167
|
+
confidence -= PENALTIES["dynamic_module"]
|
|
187
168
|
if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
|
|
188
|
-
|
|
169
|
+
confidence -= PENALTIES["test_related"]
|
|
189
170
|
|
|
190
171
|
framework_confidence = detect_framework_usage(def_obj, visitor=framework)
|
|
191
172
|
if framework_confidence is not None:
|
|
192
|
-
|
|
173
|
+
confidence = min(confidence, framework_confidence)
|
|
193
174
|
|
|
194
175
|
if (def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__")):
|
|
195
|
-
|
|
176
|
+
confidence = 0
|
|
196
177
|
|
|
197
178
|
if def_obj.type == "parameter":
|
|
198
179
|
if def_obj.simple_name in ("self", "cls"):
|
|
199
|
-
|
|
180
|
+
confidence = 0
|
|
200
181
|
elif "." in def_obj.name:
|
|
201
182
|
method_name = def_obj.name.split(".")[-2]
|
|
202
183
|
if method_name.startswith("__") and method_name.endswith("__"):
|
|
203
|
-
|
|
184
|
+
confidence = 0
|
|
204
185
|
|
|
205
186
|
if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
|
|
206
|
-
|
|
187
|
+
confidence = 0
|
|
207
188
|
|
|
208
189
|
if (def_obj.type == "import" and def_obj.name.startswith("__future__.") and
|
|
209
190
|
def_obj.simple_name in ("annotations", "absolute_import", "division",
|
|
210
191
|
"print_function", "unicode_literals", "generator_stop")):
|
|
211
|
-
|
|
192
|
+
confidence = 0
|
|
212
193
|
|
|
213
|
-
def_obj.confidence = max(
|
|
194
|
+
def_obj.confidence = max(confidence, 0)
|
|
214
195
|
|
|
215
196
|
def _apply_heuristics(self):
|
|
216
197
|
class_methods = defaultdict(list)
|
|
217
|
-
for
|
|
218
|
-
if
|
|
219
|
-
cls =
|
|
198
|
+
for definition in self.defs.values():
|
|
199
|
+
if definition.type in ("method", "function") and "." in definition.name:
|
|
200
|
+
cls = definition.name.rsplit(".", 1)[0]
|
|
220
201
|
if cls in self.defs and self.defs[cls].type == "class":
|
|
221
|
-
class_methods[cls].append(
|
|
202
|
+
class_methods[cls].append(definition)
|
|
222
203
|
|
|
223
204
|
for cls, methods in class_methods.items():
|
|
224
205
|
if self.defs[cls].references > 0:
|
|
225
|
-
for
|
|
226
|
-
if
|
|
227
|
-
|
|
206
|
+
for method in methods:
|
|
207
|
+
if method.simple_name in AUTO_CALLED:
|
|
208
|
+
method.references += 1
|
|
209
|
+
|
|
210
|
+
if (method.simple_name.startswith("visit_") or
|
|
211
|
+
method.simple_name.startswith("leave_") or
|
|
212
|
+
method.simple_name.startswith("transform_")):
|
|
213
|
+
method.references += 1
|
|
214
|
+
|
|
215
|
+
if method.simple_name == "format" and cls.endswith("Formatter"):
|
|
216
|
+
method.references += 1
|
|
228
217
|
|
|
229
218
|
def analyze(self, path, thr=60, exclude_folders=None):
|
|
230
219
|
files, root = self._get_python_files(path, exclude_folders)
|
|
@@ -251,25 +240,11 @@ class Skylos:
|
|
|
251
240
|
|
|
252
241
|
for file in files:
|
|
253
242
|
mod = modmap[file]
|
|
243
|
+
defs, refs, dyn, exports, test_flags, framework_flags = proc_file(file, mod)
|
|
254
244
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
defs, refs, dyn, exports, test_flags, framework_flags, ignored = result
|
|
259
|
-
self.ignored_lines.update(ignored)
|
|
260
|
-
else: ##legacy
|
|
261
|
-
warnings.warn(
|
|
262
|
-
"proc_file() now returns 7 values (added ignored_lines). "
|
|
263
|
-
"The 6-value form is deprecated and will disappear.",
|
|
264
|
-
DeprecationWarning,
|
|
265
|
-
stacklevel=2,
|
|
266
|
-
)
|
|
267
|
-
defs, refs, dyn, exports, test_flags, framework_flags = result
|
|
268
|
-
|
|
269
|
-
# apply penalties while we still have the file-specific flags
|
|
270
|
-
for d in defs:
|
|
271
|
-
self._apply_penalties(d, test_flags, framework_flags)
|
|
272
|
-
self.defs[d.name] = d
|
|
245
|
+
for definition in defs:
|
|
246
|
+
self._apply_penalties(definition, test_flags, framework_flags)
|
|
247
|
+
self.defs[definition.name] = definition
|
|
273
248
|
|
|
274
249
|
self.refs.extend(refs)
|
|
275
250
|
self.dynamic.update(dyn)
|
|
@@ -278,18 +253,22 @@ class Skylos:
|
|
|
278
253
|
self._mark_refs()
|
|
279
254
|
self._apply_heuristics()
|
|
280
255
|
self._mark_exports()
|
|
281
|
-
|
|
282
|
-
thr = max(0, thr)
|
|
283
256
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
continue
|
|
257
|
+
shown = 0
|
|
258
|
+
|
|
259
|
+
def def_sort_key(d):
|
|
260
|
+
return (d.type, d.name)
|
|
289
261
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
262
|
+
for d in sorted(self.defs.values(), key=def_sort_key):
|
|
263
|
+
if shown >= 50:
|
|
264
|
+
break
|
|
265
|
+
print(f" {d.type:<8} refs={d.references:<2} conf={d.confidence:<3} exported={d.is_exported} line={d.line:<4} {d.name}")
|
|
266
|
+
shown += 1
|
|
267
|
+
|
|
268
|
+
unused = []
|
|
269
|
+
for definition in self.defs.values():
|
|
270
|
+
if definition.references == 0 and not definition.is_exported and definition.confidence > 0 and definition.confidence >= thr:
|
|
271
|
+
unused.append(definition.to_dict())
|
|
293
272
|
|
|
294
273
|
result = {
|
|
295
274
|
"unused_functions": [],
|
|
@@ -299,7 +278,7 @@ class Skylos:
|
|
|
299
278
|
"unused_parameters": [],
|
|
300
279
|
"analysis_summary": {
|
|
301
280
|
"total_files": len(files),
|
|
302
|
-
"excluded_folders": exclude_folders
|
|
281
|
+
"excluded_folders": exclude_folders or [],
|
|
303
282
|
}
|
|
304
283
|
}
|
|
305
284
|
|
|
@@ -325,7 +304,6 @@ def proc_file(file_or_args, mod=None):
|
|
|
325
304
|
|
|
326
305
|
try:
|
|
327
306
|
source = Path(file).read_text(encoding="utf-8")
|
|
328
|
-
ignored = _collect_ignored_lines(source)
|
|
329
307
|
tree = ast.parse(source)
|
|
330
308
|
|
|
331
309
|
tv = TestAwareVisitor(filename=file)
|
|
@@ -333,11 +311,11 @@ def proc_file(file_or_args, mod=None):
|
|
|
333
311
|
|
|
334
312
|
fv = FrameworkAwareVisitor(filename=file)
|
|
335
313
|
fv.visit(tree)
|
|
336
|
-
|
|
314
|
+
fv.finalize()
|
|
337
315
|
v = Visitor(mod, file)
|
|
338
316
|
v.visit(tree)
|
|
339
317
|
|
|
340
|
-
return v.defs, v.refs, v.dyn, v.exports, tv, fv
|
|
318
|
+
return v.defs, v.refs, v.dyn, v.exports, tv, fv
|
|
341
319
|
except Exception as e:
|
|
342
320
|
logger.error(f"{file}: {e}")
|
|
343
321
|
if os.getenv("SKYLOS_DEBUG"):
|
|
@@ -345,55 +323,59 @@ def proc_file(file_or_args, mod=None):
|
|
|
345
323
|
dummy_visitor = TestAwareVisitor(filename=file)
|
|
346
324
|
dummy_framework_visitor = FrameworkAwareVisitor(filename=file)
|
|
347
325
|
|
|
348
|
-
return [], [], set(), set(), dummy_visitor, dummy_framework_visitor
|
|
326
|
+
return [], [], set(), set(), dummy_visitor, dummy_framework_visitor
|
|
349
327
|
|
|
350
328
|
def analyze(path,conf=60, exclude_folders=None):
|
|
351
329
|
return Skylos().analyze(path,conf, exclude_folders)
|
|
352
330
|
|
|
353
|
-
if __name__=="__main__":
|
|
331
|
+
if __name__ == "__main__":
|
|
354
332
|
if len(sys.argv)>1:
|
|
355
|
-
p=sys.argv[1]
|
|
356
|
-
|
|
333
|
+
p = sys.argv[1]
|
|
334
|
+
confidence = int(sys.argv[2]) if len(sys.argv) >2 else 60
|
|
335
|
+
result = analyze(p,confidence)
|
|
357
336
|
|
|
358
337
|
data = json.loads(result)
|
|
359
|
-
print("\n
|
|
338
|
+
print("\n Python Static Analysis Results")
|
|
360
339
|
print("===================================\n")
|
|
361
340
|
|
|
362
|
-
total_items =
|
|
341
|
+
total_items = 0
|
|
342
|
+
for key, items in data.items():
|
|
343
|
+
if key.startswith("unused_") and isinstance(items, list):
|
|
344
|
+
total_items += len(items)
|
|
363
345
|
|
|
364
346
|
print("Summary:")
|
|
365
347
|
if data["unused_functions"]:
|
|
366
|
-
print(f"
|
|
348
|
+
print(f" * Unreachable functions: {len(data['unused_functions'])}")
|
|
367
349
|
if data["unused_imports"]:
|
|
368
|
-
print(f"
|
|
350
|
+
print(f" * Unused imports: {len(data['unused_imports'])}")
|
|
369
351
|
if data["unused_classes"]:
|
|
370
|
-
print(f"
|
|
352
|
+
print(f" * Unused classes: {len(data['unused_classes'])}")
|
|
371
353
|
if data["unused_variables"]:
|
|
372
|
-
print(f"
|
|
354
|
+
print(f" * Unused variables: {len(data['unused_variables'])}")
|
|
373
355
|
|
|
374
356
|
if data["unused_functions"]:
|
|
375
|
-
print("\n
|
|
357
|
+
print("\n - Unreachable Functions")
|
|
376
358
|
print("=======================")
|
|
377
359
|
for i, func in enumerate(data["unused_functions"], 1):
|
|
378
360
|
print(f" {i}. {func['name']}")
|
|
379
361
|
print(f" └─ {func['file']}:{func['line']}")
|
|
380
362
|
|
|
381
363
|
if data["unused_imports"]:
|
|
382
|
-
print("\n
|
|
364
|
+
print("\n - Unused Imports")
|
|
383
365
|
print("================")
|
|
384
366
|
for i, imp in enumerate(data["unused_imports"], 1):
|
|
385
367
|
print(f" {i}. {imp['simple_name']}")
|
|
386
368
|
print(f" └─ {imp['file']}:{imp['line']}")
|
|
387
369
|
|
|
388
370
|
if data["unused_classes"]:
|
|
389
|
-
print("\n
|
|
371
|
+
print("\n - Unused Classes")
|
|
390
372
|
print("=================")
|
|
391
373
|
for i, cls in enumerate(data["unused_classes"], 1):
|
|
392
374
|
print(f" {i}. {cls['name']}")
|
|
393
375
|
print(f" └─ {cls['file']}:{cls['line']}")
|
|
394
376
|
|
|
395
377
|
if data["unused_variables"]:
|
|
396
|
-
print("\n
|
|
378
|
+
print("\n - Unused Variables")
|
|
397
379
|
print("==================")
|
|
398
380
|
for i, var in enumerate(data["unused_variables"], 1):
|
|
399
381
|
print(f" {i}. {var['name']}")
|
|
@@ -406,7 +388,7 @@ if __name__=="__main__":
|
|
|
406
388
|
print(f"```")
|
|
407
389
|
|
|
408
390
|
print("\nNext steps:")
|
|
409
|
-
print("
|
|
410
|
-
print("
|
|
391
|
+
print(" * Use --interactive to select specific items to remove")
|
|
392
|
+
print(" * Use --dry-run to preview changes before applying them")
|
|
411
393
|
else:
|
|
412
394
|
print("Usage: python Skylos.py <path> [confidence_threshold]")
|