Skip to content

Commit

Permalink
feat: ✨ Issue 7 add basic repository browser (#8)
Browse files Browse the repository at this point in the history
* WIP

* Show units in browser

* Fixed tests

* Add source location

* Show code snippet

* Very basic report browser
  • Loading branch information
robvanderleek authored Jan 14, 2023
1 parent 223a964 commit 9645be2
Show file tree
Hide file tree
Showing 31 changed files with 699 additions and 307 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
build/
dist/
clim.spec
main.spec
codelimit.json
.coverage
coverage.xml
2 changes: 2 additions & 0 deletions build-dist.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/sh
poetry run pyinstaller --specpath dist -n codelimit -F main.py
30 changes: 0 additions & 30 deletions clim

This file was deleted.

6 changes: 3 additions & 3 deletions codelimit/common/Codebase.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
class Codebase:
def __init__(self):
self.tree = {'./': SourceFolder()}
self.measurements = {}
self.measurements: dict[str, list[SourceMeasurement]] = {}

def add_file(self, path: str, measurements: list[SourceMeasurement]):
self.measurements[path] = measurements
Expand Down Expand Up @@ -52,8 +52,8 @@ def all_measurements(self) -> list[SourceMeasurement]:
result.extend(m)
return result

def all_measurements_sorted_by_length(self):
return sorted(self.all_measurements(), key=lambda m: m.value)
def all_measurements_sorted_by_length_asc(self):
return sorted(self.all_measurements(), key=lambda m: m.value, reverse=True)

def total_loc(self) -> int:
result = 0
Expand Down
7 changes: 6 additions & 1 deletion codelimit/common/Scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from codelimit.common.Codebase import Codebase
from codelimit.common.SourceMeasurement import SourceMeasurement
from codelimit.common.scope_utils import build_scopes
from codelimit.common.source_utils import get_location_range
from codelimit.languages.c.CLanguage import CLanguage
from codelimit.languages.python.PythonLaguage import PythonLanguage

Expand Down Expand Up @@ -52,5 +53,9 @@ def _scan_file(self, language, root, folder, file):
measurements = []
for scope in scopes:
length = len(scope)
measurements.append(SourceMeasurement(scope.header.tokens[0].location.line, length))
unit_name = get_location_range(code, scope.header.tokens[0].location, scope.block.tokens[0].location)
unit_name = unit_name.strip().replace('\t', ' ').replace('\n', ' ')
start_location = scope.header.tokens[0].location
end_location = scope.block.tokens[-1].location
measurements.append(SourceMeasurement(unit_name, start_location, end_location, length))
self.codebase.add_file(rel_path, measurements)
3 changes: 3 additions & 0 deletions codelimit/common/SourceLocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ def __init__(self, line: int, column: int):
def __str__(self):
return f'{{line: {self.line}, column: {self.column}}}'

def __repr__(self):
return self.__str__()

def lt(self, other: SourceLocation):
return self.line < other.line or (self.line == other.line and self.column < other.column)

Expand Down
6 changes: 5 additions & 1 deletion codelimit/common/SourceMeasurement.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from dataclasses import dataclass

from codelimit.common.SourceLocation import SourceLocation


@dataclass
class SourceMeasurement:
start_line: int
unit_name: str
start: SourceLocation
end: SourceLocation
value: int
26 changes: 26 additions & 0 deletions codelimit/common/report/Browser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import os.path
from pathlib import Path

from InquirerPy import inquirer
from InquirerPy.base import Choice

from codelimit.common.report.Report import Report
from codelimit.common.report.ReportUnit import format_report_unit, ReportUnit
from codelimit.common.source_utils import get_location_range


class Browser:
def __init__(self, report: Report, path: Path = '.'):
self.report = report
self.path = path

def show(self):
units = [Choice(value=unit, name=format_report_unit(unit)) for unit in
self.report.all_report_units_sorted_by_length_asc()]
while True:
selected_unit: ReportUnit = inquirer.select(message='Select unit', choices=units).execute()
file_path = os.path.join(self.path, selected_unit['file'])
with open(file_path) as file:
code = file.read()
snippet = get_location_range(code, selected_unit['measurement']['start'], selected_unit['measurement']['end'])
print(snippet)
13 changes: 11 additions & 2 deletions codelimit/common/Report.py → codelimit/common/report/Report.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import plotext

from codelimit.common.Codebase import Codebase
from codelimit.common.report.ReportUnit import ReportUnit
from codelimit.common.utils import make_profile


Expand All @@ -18,7 +19,7 @@ def get_average(self):
return ceil(self.codebase.total_loc() / len(self.codebase.all_measurements()))

def ninetieth_percentile(self):
sorted_measurements = self.codebase.all_measurements_sorted_by_length()
sorted_measurements = self.codebase.all_measurements_sorted_by_length_asc()
lines_of_code_90_percent = floor(self.codebase.total_loc() * 0.9)
smallest_units_loc = 0
for index, m in enumerate(sorted_measurements):
Expand All @@ -27,6 +28,14 @@ def ninetieth_percentile(self):
return sorted_measurements[index].value
return 0

def all_report_units_sorted_by_length_asc(self) -> list[ReportUnit]:
result = []
for file, measurements in self.codebase.measurements.items():
for m in measurements:
result.append(ReportUnit(file, m))
result = sorted(result, key=lambda unit: unit.measurement.value, reverse=True)
return result

def risk_categories(self):
return make_profile(self.codebase.all_measurements())

Expand All @@ -35,4 +44,4 @@ def display_risk_category_plot(self):
volume = make_profile(self.codebase.all_measurements())
plotext.title("Most Favored Pizzas in the World")
plotext.simple_bar(labels, volume, color=[34, 226, 214, 196])
plotext.show()
plotext.show()
24 changes: 24 additions & 0 deletions codelimit/common/report/ReportReader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from json import loads

from codelimit.common.Codebase import Codebase
from codelimit.common.SourceLocation import SourceLocation
from codelimit.common.report.Report import Report
from codelimit.common.SourceMeasurement import SourceMeasurement


class ReportReader:

@staticmethod
def from_json(json: str) -> Report:
d = loads(json)
codebase = Codebase()
report = Report(codebase)
report.uuid = d['uuid']
for k, v in d['codebase']['measurements'].items():
measurements: list[SourceMeasurement] = []
for m in v:
start_location = SourceLocation(m['start']['line'], m['start']['column'])
end_location = SourceLocation(m['end']['line'], m['end']['column'])
measurements.append(SourceMeasurement(m['unit_name'], start_location, end_location, m['value']))
codebase.add_file(k, measurements)
return report
16 changes: 16 additions & 0 deletions codelimit/common/report/ReportUnit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from dataclasses import dataclass

from codelimit.common.SourceMeasurement import SourceMeasurement


@dataclass
class ReportUnit:
file: str
measurement: SourceMeasurement


def format_report_unit(unit: ReportUnit) -> str:
name = unit.measurement.unit_name
length = unit.measurement.value
prefix = f'[{length:3}]' if length < 61 else '[60+]'
return f'{prefix} {name}'
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from codelimit.common.Report import Report
from codelimit.common.SourceFolder import SourceFolder
from codelimit.common.SourceFolderEntry import SourceFolderEntry
from codelimit.common.SourceMeasurement import SourceMeasurement
from codelimit.common.report.Report import Report


class ReportSerializer:
class ReportWriter:
def __init__(self, report: Report, pretty_print=True):
self.report = report
self.tree = report.codebase.tree
Expand Down Expand Up @@ -86,13 +86,17 @@ def _measurement_item_to_json(self, name: str, measurements: list[SourceMeasurem
json += self._close(']')
return json

def _measurement_to_json(self, measurement: SourceMeasurement) -> str:
json = ''
json += f'{{"unit_name": "{measurement.unit_name}", '
json += f'"start": {{"line": {measurement.start.line}, "column": {measurement.start.column}}}, '
json += f'"end": {{"line": {measurement.end.line}, "column": {measurement.end.column}}}, '
json += f'"value": {measurement.value}}}'
return self._line(json)

def _source_folder_entry_to_json(self, entry: SourceFolderEntry) -> str:
json = f'{{"name": "{entry.name}"'
if entry.profile:
json += f', "profile": {entry.profile}'
json += '}'
return self._line(json)

def _measurement_to_json(self, measurement: SourceMeasurement) -> str:
json = f'{{"start_line": {measurement.start_line}, "value": {measurement.value}}}'
return self._line(json)
Empty file.
38 changes: 22 additions & 16 deletions codelimit/common/scope_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from codelimit.common.Language import Language
from codelimit.common.Scope import Scope
from codelimit.common.TokenRange import TokenRange
from codelimit.common.Language import Language
from codelimit.common.token_utils import sort_tokens
from codelimit.common.utils import delete_indices


def build_scopes(language: Language, code: str) -> list[Scope]:
Expand All @@ -12,20 +14,24 @@ def build_scopes(language: Language, code: str) -> list[Scope]:


def _build_scopes_from_headers_and_blocks(headers: list[TokenRange], blocks: list[TokenRange]) -> list[Scope]:
result = []
reverse_blocks = blocks[::-1]
for header in headers[::-1]:
scope_blocks = []
for index, block in enumerate(reverse_blocks):
if block.lt(header) or block.overlaps(header):
reverse_blocks = reverse_blocks[index:]
break
if block.tokens[0].location.column > header.tokens[0].location.column:
scope_blocks = block.tokens + scope_blocks
else:
scope_blocks = []
if len(scope_blocks) > 0:
scope_block = TokenRange(scope_blocks)
result.append(Scope(header, scope_block))
result: list[Scope] = []
reverse_headers = headers[::-1]
for header in reverse_headers:
scope_blocks_indices = _find_scope_blocks_indices(header, blocks)
scope_tokens = []
for i in scope_blocks_indices:
scope_tokens.extend(blocks[i].tokens)
scope_tokens = sort_tokens(scope_tokens)
scope_block = TokenRange(scope_tokens)
result.append(Scope(header, scope_block))
blocks = delete_indices(blocks, scope_blocks_indices)
result.reverse()
return result


def _find_scope_blocks_indices(header: TokenRange, blocks: list[TokenRange]) -> list[int]:
blocks_after_header = [i for i in range(len(blocks)) if header.lt(blocks[i])]
if len(blocks_after_header) > 0:
body_blocks = [i for i in blocks_after_header if blocks[blocks_after_header[0]].overlaps(blocks[i])]
return body_blocks
return []
15 changes: 10 additions & 5 deletions codelimit/common/source_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from codelimit.common.SourceLocation import SourceLocation
from codelimit.common.Token import Token
from codelimit.common.TokenRange import TokenRange


def get_newline_indices(code: str) -> list[int]:
Expand Down Expand Up @@ -38,10 +37,16 @@ def location_to_index(code: str, position: SourceLocation) -> int:
return result


def get_range(code: str, source_range: TokenRange) -> str:
start_index = location_to_index(code, source_range.tokens[0].location)
end_index = location_to_index(code, source_range.tokens[-1].location)
return code[start_index:end_index + len(source_range.tokens[-1].value)]
def get_token_range(code: str, start: Token, end: Token) -> str:
start_index = location_to_index(code, start.location)
end_index = location_to_index(code, end.location)
return code[start_index:end_index + len(end.value)]


def get_location_range(code: str, start: SourceLocation, end: SourceLocation) -> str:
start_index = location_to_index(code, start)
end_index = location_to_index(code, end)
return code[start_index:end_index]


def filter_tokens(tokens: list[Token], predicate: Callable[[Token], bool]) -> list[Token]:
Expand Down
6 changes: 6 additions & 0 deletions codelimit/common/token_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ def get_balanced_symbol_token_indices(tokens: list[Token], start: str, end: str,
if nested or len(block_starts) == 0:
result.append((start_index, index))
return result


def sort_tokens(tokens: list[Token]) -> list[Token]:
result = sorted(tokens, key=lambda t: t.location.column)
result = sorted(result, key=lambda t: t.location.line)
return result
8 changes: 6 additions & 2 deletions codelimit/common/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
from typing import Union
from typing import Union, Any

from codelimit.common.SourceMeasurement import SourceMeasurement

Expand Down Expand Up @@ -34,7 +34,7 @@ def path_has_suffix(path: str, suffixes: Union[str, list[str]]):
return False


def get_parent_folder(path: str) -> Union[str, None]:
def get_parent_folder(path: str) -> str:
parts = path.split(os.path.sep)
if len(parts) == 1:
return '.'
Expand All @@ -45,3 +45,7 @@ def get_parent_folder(path: str) -> Union[str, None]:
def get_basename(path: str) -> str:
parts = path.split(os.path.sep)
return parts[-1]


def delete_indices(iterable: list, indices: list[int]) -> list[Any]:
return [b for i, b in enumerate(iterable) if i not in indices]
5 changes: 4 additions & 1 deletion codelimit/languages/c/CScopeExtractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ def has_curly_suffix(index):

def extract_blocks(self, tokens: list[Token]) -> list[TokenRange]:
balanced_tokens = get_balanced_symbol_token_indices(tokens, '{', '}', True)
return [TokenRange(tokens[bt[0]:bt[1] + 1]) for bt in balanced_tokens]
blocks = [TokenRange(tokens[bt[0]:bt[1] + 1]) for bt in balanced_tokens]
sorted_by_line = sorted(blocks, key=lambda tr: tr.tokens[0].location.line)
sorted_by_columns = sorted(sorted_by_line, key=lambda tr: tr.tokens[0].location.column)
return sorted_by_columns
Loading

0 comments on commit 9645be2

Please sign in to comment.