check_routes.py 4.65 KB
Newer Older
1 2 3 4 5
#!/usr/bin/env python3
import regex as re
import os
import sys

Robin Sonnabend's avatar
Robin Sonnabend committed
6 7 8 9
ROUTE_PATTERN = (
    r'@(?:[[:alpha:]])+\.route\(\"(?<url>[^"]+)"[^)]*\)\s*'
    r'(?:@[[:alpha:]_()., ]+\s*)*def\s+(?<name>[[:alpha:]][[:alnum:]_]*)'
    r'\((?<params>[[:alnum:], ]*)\):')
10
quote_group = "[\"']"
Robin Sonnabend's avatar
Robin Sonnabend committed
11 12 13
URL_FOR_PATTERN = (
    r'url_for\({quotes}(?<name>[[:alpha:]][[:alnum:]_]*)'
    '{quotes}'.format(quotes=quote_group))
14 15 16 17 18

ROOT_DIR = "."
ENDINGS = [".py", ".html", ".txt"]
MAX_DEPTH = 2

Robin Sonnabend's avatar
Robin Sonnabend committed
19

20 21 22 23 24 25 26 27 28 29 30 31
def list_dir(dir, level=0):
    if level >= MAX_DEPTH:
        return
    for file in os.listdir(dir):
        path = os.path.join(dir, file)
        if os.path.isfile(path):
            if file == sys.argv[0]:
                continue
            for ending in ENDINGS:
                if file.endswith(ending):
                    yield path
        elif os.path.isdir(path):
Robin Sonnabend's avatar
Robin Sonnabend committed
32 33
            yield from list_dir(path, level + 1)

34 35 36 37 38 39 40 41 42 43 44 45 46 47

class Route:
    def __init__(self, file, name, parameters):
        self.file = file
        self.name = name
        self.parameters = parameters

    def __repr__(self):
        return "Route({file}, {name}, {parameters})".format(
            file=self.file, name=self.name, parameters=self.parameters)

    def get_parameter_set(self):
        return {parameter.name for parameter in self.parameters}

Robin Sonnabend's avatar
Robin Sonnabend committed
48

49 50 51 52 53 54
class Parameter:
    def __init__(self, name, type=None):
        self.name = name
        self.type = type

    def __repr__(self):
Robin Sonnabend's avatar
Robin Sonnabend committed
55 56
        return "Parameter({name}, {type})".format(
            name=self.name, type=self.type)
57 58 59 60 61 62 63 64

    @staticmethod
    def from_string(text):
        if ":" in text:
            type, name = text.split(":", 1)
            return Parameter(name, type)
        return Parameter(text)

Robin Sonnabend's avatar
Robin Sonnabend committed
65

66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
def split_url_parameters(url):
    params = []
    current_param = None
    for char in url:
        if current_param is None:
            if char == "<":
                current_param = ""
        else:
            if char == ">":
                params.append(Parameter.from_string(current_param))
                current_param = None
            else:
                current_param += char
    return params

Robin Sonnabend's avatar
Robin Sonnabend committed
81

82 83 84
def split_function_parameters(parameters):
    return list(map(str.strip, parameters.split(",")))

Robin Sonnabend's avatar
Robin Sonnabend committed
85

86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
def read_url_for_parameters(content):
    params = []
    bracket_level = 1
    current_param = None
    for char in content:
        if bracket_level == 0:
            if current_param is not None:
                params.append(current_param.split("=")[0].strip())
            return params
        if char == ",":
            if current_param is not None:
                params.append(current_param.split("=")[0].strip())
            current_param = ""
        else:
            if current_param is not None:
                current_param += char
            if char == "(":
                bracket_level += 1
            elif char == ")":
                bracket_level -= 1

Robin Sonnabend's avatar
Robin Sonnabend committed
107

108 109 110 111 112 113 114
class UrlFor:
    def __init__(self, file, name, parameters):
        self.file = file
        self.name = name
        self.parameters = parameters

    def __repr__(self):
Robin Sonnabend's avatar
Robin Sonnabend committed
115 116 117 118
        return (
            "UrlFor(file={file}, name={name}, parameters={parameters})".format(
                file=self.file, name=self.name, parameters=self.parameters))

119 120 121 122 123 124 125 126

routes = {}
url_fors = []
for file in list_dir(ROOT_DIR):
    with open(file, "r") as infile:
        content = infile.read()
        for match in re.finditer(ROUTE_PATTERN, content):
            name = match.group("name")
Robin Sonnabend's avatar
Robin Sonnabend committed
127 128
            function_parameters = split_function_parameters(
                match.group("params"))
129 130 131 132 133 134
            url_parameters = split_url_parameters(match.group("url"))
            routes[name] = Route(file, name, url_parameters)
        for match in re.finditer(URL_FOR_PATTERN, content):
            name = match.group("name")
            begin, end = match.span()
            parameters = read_url_for_parameters(content[end:])
Robin Sonnabend's avatar
Robin Sonnabend committed
135 136 137
            url_fors.append(UrlFor(
                file=file, name=name, parameters=parameters))

138 139 140

for url_for in url_fors:
    if url_for.name not in routes:
Robin Sonnabend's avatar
Robin Sonnabend committed
141 142
        print("Missing route '{}' (for url_for in '{}')".format(
            url_for.name, url_for.file))
143 144 145 146 147
        continue
    route = routes[url_for.name]
    route_parameters = route.get_parameter_set()
    url_parameters = set(url_for.parameters)
    if len(route_parameters ^ url_parameters) > 0:
Robin Sonnabend's avatar
Robin Sonnabend committed
148 149
        print("Parameters not matching for '{}' in '{}:'".format(
            url_for.name, url_for.file))
150 151 152 153 154 155
        only_route = route_parameters - url_parameters
        only_url = url_parameters - route_parameters
        if len(only_route) > 0:
            print("Only in route: {}".format(only_route))
        if len(only_url) > 0:
            print("Only in url: {}".format(only_url))