Skip to content

Commit

Permalink
Create a LineLoc dataclass to store the line num and filename for lin…
Browse files Browse the repository at this point in the history
…es and functions. Use this to more precisely deal with locations of functions and their references.
  • Loading branch information
johndoknjas committed May 10, 2024
1 parent a891661 commit 77ab29f
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 26 deletions.
36 changes: 25 additions & 11 deletions lintception/Utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,20 @@
from dataclasses import dataclass
from typing import Optional

@dataclass
class LineLoc:
line_index: int
filename: str

@dataclass
class Func:
name: str
line_index: int
line_loc: LineLoc

@dataclass
class Line:
line_loc: LineLoc
line_str: str

def assertions_for_settings_dict(settings: dict) -> None:
assert (settings.keys() == {'MinVersion', 'NumIncompatibleVersions'} and
Expand All @@ -18,8 +28,11 @@ def is_code_line(line: str) -> bool:
return (bool(line.strip()) and not line.lstrip().startswith(('#', '"""')) and
not line.rstrip().endswith('"""'))

def get_python_filenames() -> list[str]:
return list(glob.iglob('**/*.py', recursive=True))

def num_python_files() -> int:
return len(list(glob.iglob('**/*.py', recursive=True)))
return len(get_python_filenames())

def read_json_file(filename: str) -> dict:
"""Returns the dict represented by the json. If the file doesn't exist, returns an empty dict."""
Expand All @@ -29,23 +42,24 @@ def read_json_file(filename: str) -> dict:
except FileNotFoundError:
return {}

def get_lines_all_py_files(filenames_exclude: Optional[list[str]] = None) -> list[str]:
def get_lines_all_py_files(filenames_exclude: Optional[list[str]] = None) -> list[Line]:
lines = []
for filename in glob.iglob('**/*.py', recursive=True):
for filename in get_python_filenames():
if filenames_exclude and filename in filenames_exclude:
continue
with open(filename) as file:
lines.extend(file.read().splitlines())
for i, line_str in enumerate(file.read().splitlines()):
lines.append(Line(LineLoc(i+1, filename), line_str))
return lines

def find_funcs(lines: list[str]) -> list[Func]:
"""`lines` are all the lines of code. The function will go through it and find all function definitions,
putting each function name and line index into the list that's returned."""
def find_funcs(lines: list[Line]) -> list[Func]:
"""`lines` are all the lines in all the .py files. The function will go through it and find all
function definitions, putting each function name and line location into the list that's returned."""
funcs: list[Func] = []
for i, code_line in enumerate(lines):
words = code_line.split()
for line in lines:
words = line.line_str.split()
if not words or words[0] != 'def':
continue
assert '(' in words[1]
funcs.append(Func(words[1].split('(')[0], i))
funcs.append(Func(words[1].split('(')[0], line.line_loc))
return funcs
13 changes: 7 additions & 6 deletions lintception/linters.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
from __future__ import annotations
import subprocess
from subprocess import PIPE
import glob
import vulture # type: ignore
import mypy.api
from enum import Enum
from itertools import count

from . import Utils
from .Utils import Func
from .Utils import Func, Line

class LintResult(Enum):
SUCCESS, MYPY_ERR, VULTURE_ERR, VERMIN_ERR, NO_FUTURE_ANNOT_ERR, NO_FUNC_ANNOT_ERR = range(6)
Expand All @@ -33,7 +31,7 @@ def test_vermin(settings: dict) -> bool:
(result.returncode, result.stderr) == (0, ''))

def test_future_annotations() -> bool:
for filename in glob.iglob('**/*.py', recursive=True):
for filename in Utils.get_python_filenames():
assert filename.endswith(".py")
with open(filename) as file:
first_code_line = next(
Expand All @@ -46,8 +44,11 @@ def test_future_annotations() -> bool:
return False
return True

def func_has_annotations(lines: list[str], func: Func) -> bool:
if ') -> ' not in next(lines[i] for i in count(func.line_index) if ')' in lines[i]):
def func_has_annotations(lines: list[Line], func: Func) -> bool:
lines = sorted([l for l in lines if l.line_loc.filename == func.line_loc.filename and
l.line_loc.line_index >= func.line_loc.line_index],
key=lambda l: l.line_loc.line_index)
if ') -> ' not in next(l.line_str for l in lines if ')' in l.line_str):
print(f"{str(func)} doesn't have a return type annotation")
return False
return True
Expand Down
2 changes: 1 addition & 1 deletion lintception/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def main() -> None:
if (result := linters.run_linters()) != linters.LintResult.SUCCESS:
print(f"Error: {result.name}")
sys.exit(0)
print('vulture, mypy, and vermin found no errors.')
print('\nvulture, mypy, and vermin found no errors.')
print('Also, all python files have a future annotations import at the top, and ', end='')
print('all functions have a return type annotation.\n')
my_linter.main()
Expand Down
27 changes: 19 additions & 8 deletions lintception/my_linter.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
from __future__ import annotations
from . import Utils
from .Utils import Func
from .Utils import Func, Line

def find_func_references(lines: list[str], func: Func) -> list[int]:
def find_func_references(lines: list[Line], func: Func) -> list[Line]:
"""Note that this doesn't include the function's definition."""
return [i for (i, line) in enumerate(lines) if func.name in line and i != func.line_index]
return [l for l in lines if func.name in l.line_str and func.line_loc != l.line_loc]

def func_ref_distance(elem: tuple[Func, Line]) -> tuple[int, int, int]:
"""Returns a 'greater' tuple if the 'distance' between the function and reference is greater.
Order of properties by importance: being in diff files, the reference being before the function,
and the line distance between the two. The latter properties are only considered if the func and ref
are in the same file."""
in_same_file = elem[0].line_loc.filename == elem[1].line_loc.filename
if not in_same_file:
return (1,1,1)
ref_after_func = elem[0].line_loc.line_index < elem[1].line_loc.line_index
line_abs_dist = abs(elem[0].line_loc.line_index - elem[1].line_loc.line_index)
return (-1, (-1 if ref_after_func else 1), line_abs_dist)

def main() -> None:
lines = Utils.get_lines_all_py_files(["tests.py"])
funcs: list[Func] = Utils.find_funcs(lines)
funcs_used_once: list[tuple[Func, int]] = []
funcs_used_once: list[tuple[Func, Line]] = []
print("\n\nUnused functions:\n")
for func in funcs:
references = find_func_references(lines, func)
if len(references) == 0:
print(f"******{func} is unused******")
elif len(references) == 1:
funcs_used_once.append((func, references[0]))
funcs_used_once.sort(key=lambda f:
((defined_vs_used := f[0].line_index-f[1]) < 0, -abs(defined_vs_used)),
reverse=True)
funcs_used_once.sort(key=func_ref_distance)
print("\n\nFunctions used only once:\n")
for f in funcs_used_once:
print(f"{f[0]} is only referenced at line index {f[1]}")
print(f"Func {f[0].name} (line {f[0].line_loc.line_index} of {f[0].line_loc.filename}) " +
f"is only referenced at line {f[1].line_loc.line_index} of {f[1].line_loc.filename}")

0 comments on commit 77ab29f

Please sign in to comment.