Skip to content
This repository was archived by the owner on Jun 23, 2021. It is now read-only.

Commit 2bef2e6

Browse files
committed
Initial commit
0 parents  commit 2bef2e6

11 files changed

+295
-0
lines changed

lint381/__init__.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""Lint source code for EECS 381.
2+
3+
See http://umich.edu/~eecs381/ for the course homepage. The coding standards
4+
can be found here:
5+
6+
* C: http://umich.edu/~eecs381/handouts/C_Coding_Standards.pdf
7+
* C++: http://umich.edu/~eecs381/handouts/C++_Coding_Standards.pdf
8+
"""

lint381/__main__.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Run the linter on the specified source code files."""
2+
import click
3+
4+
from .c import linter as c_linter
5+
6+
7+
@click.command()
8+
@click.argument("files", nargs=-1, type=click.File())
9+
def main(files):
10+
"""Lint the files specified on the command-line."""
11+
for file in files:
12+
lines = file.read().splitlines()
13+
14+
if file.name.endswith(".c"):
15+
errors = c_linter.lint(lines)
16+
elif file.name.endswith(".cpp"):
17+
raise NotImplementedError()
18+
else:
19+
click.secho("Unknown file type: {}"
20+
.format(file.name),
21+
fg="red")
22+
23+
for line_num, errors in sorted(errors.items()):
24+
# 1-index the line number.
25+
line_num = line_num + 1
26+
27+
for error_message in errors:
28+
message = ""
29+
message += click.style("{}:{}: "
30+
.format(file.name, line_num),
31+
bold=True)
32+
message += click.style("error: ",
33+
fg="red",
34+
bold=True)
35+
message += error_message
36+
click.echo(message)

lint381/c.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""C linters."""
2+
from .code import match_tokens
3+
from .linter import Linter
4+
5+
linter = Linter()
6+
7+
8+
@linter.register
9+
def underscore_define(window):
10+
"""Find #defines that start with underscores."""
11+
match = match_tokens(window, "#define <macro>")
12+
if not match:
13+
return
14+
15+
macro = match["macro"]
16+
if macro.startswith("_"):
17+
return ("Macro `{}` should not start with an underscore"
18+
.format(macro))

lint381/code.py

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Tools for matching code patterns."""
2+
3+
4+
def tokenize(string):
5+
"""Split a string into tokens.
6+
7+
Note that tokenizing C++ is really hard. This doesn't even try to be good
8+
at it.
9+
10+
TODO: Tokenize strings and comments correctly.
11+
12+
:param str string: The string to tokenize. This could be a line or a file.
13+
:returns list: A list of tokens, each of which is a string.
14+
"""
15+
return string.strip().split()
16+
17+
18+
def match_tokens(window, pattern):
19+
"""Match a pattern of tokens on the current window.
20+
21+
:param lint381.linter.Window window: The window onto the source code.
22+
:param str pattern: The pattern to match. TODO: Provide example.
23+
:returns dict: A dictionary of the matched pattern values, or `None` if
24+
there was no match.
25+
"""
26+
search_tokens = pattern.split()
27+
28+
def ngrams(seq, n):
29+
parts = []
30+
for i in range(n):
31+
parts.append(seq[i:])
32+
return zip(*parts)
33+
34+
def match_local(sequence):
35+
variables = {}
36+
for variable, token in zip(search_tokens, candidate):
37+
# TODO: Might not be able to use < and > because of template
38+
# parameters.
39+
if variable.startswith("<") and variable.endswith(">"):
40+
variable = variable[1:-1]
41+
variables[variable] = token
42+
else:
43+
if variable != token:
44+
return None
45+
return variables
46+
47+
for candidate in ngrams(window.tokens, n=len(search_tokens)):
48+
match = match_local(candidate)
49+
if match:
50+
return match
51+
return None

lint381/linter.py

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Linter tools."""
2+
import collections
3+
4+
from .code import tokenize
5+
6+
7+
class Window(collections.namedtuple("Window", ["lines", "line_num"])):
8+
"""A window onto a segment of the source code.
9+
10+
:ivar list lines: The list of lines in the file.
11+
:ivar int line_num: The line number we're currently looking at.
12+
"""
13+
14+
@property
15+
def line(self):
16+
"""The line we're currently looking at."""
17+
return self.lines[self.line_num]
18+
19+
@property
20+
def tokens(self):
21+
"""The tokens in the current line."""
22+
return tokenize(self.line)
23+
24+
25+
class Linter:
26+
"""A registry of linters for a specific language."""
27+
28+
def __init__(self):
29+
"""Initialize the linter registry to be empty."""
30+
self._linters = []
31+
32+
def register(self, func):
33+
"""Register the provided function as a linter.
34+
35+
This should be used as a decorator:
36+
37+
```
38+
linters = Linter()
39+
40+
@linters.register
41+
def linter():
42+
...
43+
```
44+
45+
:param function func: The linting function.
46+
"""
47+
self._linters.append(func)
48+
return func
49+
50+
def lint(self, lines):
51+
"""Find linting errors on the specified lines of source code."""
52+
errors = collections.defaultdict(list)
53+
54+
for line_num, _ in enumerate(lines):
55+
window = Window(lines=lines, line_num=line_num)
56+
for func in self._linters:
57+
error_message = func(window)
58+
if error_message:
59+
errors[line_num].append(error_message)
60+
61+
return errors

requirements-dev.txt

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
flake8==2.5.1
2+
flake8-import-order==0.6.1
3+
flake8-pep257==1.0.5
4+
5+
pytest==2.8.5
6+
pytest-cov==2.2.0
7+
pytest-pythonpath==0.7

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
click==6.2

setup.cfg

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[coverage:report]
2+
show_missing = True
3+
4+
[coverage:run]
5+
branch = True
6+
source = lint381
7+
8+
[flake8]
9+
application-import-names = lint381
10+
import-order-style = google
11+
12+
[pep257]
13+
ignore = D203
14+
15+
[pytest]
16+
addopts = --cov
17+
python_paths = .

setup.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Install the `lint381` script."""
2+
import os
3+
4+
from setuptools import setup
5+
6+
7+
def get_requirements(requirements_filename):
8+
"""Get the list of requirements from a requirements file.
9+
10+
:param str requirements_filename: The name of the requirements file, such
11+
as `requirements.txt`.
12+
:returns list: A list of dependencies in the requirements file.
13+
"""
14+
setup_dir = os.path.dirname(os.path.abspath(__file__))
15+
requirements_path = os.path.join(setup_dir, requirements_filename)
16+
with open(requirements_path) as f:
17+
return [i
18+
for i in f.read().splitlines()
19+
if i
20+
if not i.startswith("#")]
21+
22+
setup(
23+
name="lint381",
24+
version="0.1.0",
25+
author="Waleed Khan",
26+
author_email="[email protected]",
27+
description="C and C++ linter for EECS 381.",
28+
url="https://github.com/arxanas/lint381",
29+
30+
packages=["lint381"],
31+
entry_points="""
32+
[console_scripts]
33+
lint381=lint381.__main__:main
34+
""",
35+
install_requires=get_requirements("requirements.txt"),
36+
)

test/test_c.py

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Test the C linters."""
2+
from lint381.c import linter
3+
4+
5+
def assert_has_error(line, error_message):
6+
"""Assert that the given line of source code has an error.
7+
8+
:param str line: The line of source code.
9+
:param str error_message: The expected error message.
10+
"""
11+
lines = [line]
12+
assert linter.lint(lines) == {0: [error_message]}
13+
14+
15+
def assert_no_error(line):
16+
"""Assert that the given line of source code doesn't have an error.
17+
18+
:param str line: The line of source code.
19+
"""
20+
lines = [line]
21+
assert not linter.lint(lines)
22+
23+
24+
def test_correct_source_code():
25+
"""Ensure that correct source code doesn't raise errors."""
26+
code = """
27+
int main() {
28+
return 0
29+
}
30+
"""
31+
lines = code.strip().splitlines()
32+
assert not linter.lint(lines)
33+
34+
35+
def test_macro_underscore():
36+
"""Ensure that macros don't start with underscores."""
37+
assert_has_error("#define __FOO__",
38+
"Macro `__FOO__` should not start with an underscore")
39+
assert_no_error("#define BAR")

test/test_linter.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Test the linter tools."""
2+
from lint381.linter import Linter
3+
4+
5+
def test_linter():
6+
"""Ensure that the linter registers and applies all linting functions."""
7+
linter = Linter()
8+
9+
@linter.register
10+
def foo(window):
11+
if window.line_num == 0:
12+
return "foo"
13+
14+
@linter.register
15+
def bar(window):
16+
return "bar"
17+
18+
assert linter.lint(["foo", "bar"]) == {
19+
0: ["foo", "bar"],
20+
1: ["bar"],
21+
}

0 commit comments

Comments
 (0)