This repository was archived by the owner on Jun 23, 2021. It is now read-only.
File tree 11 files changed +295
-0
lines changed
11 files changed +295
-0
lines changed Original file line number Diff line number Diff line change
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
+ """
Original file line number Diff line number Diff line change
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 )
Original file line number Diff line number Diff line change
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 ))
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
1
+ click == 6.2
Original file line number Diff line number Diff line change
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 = .
Original file line number Diff line number Diff line change
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
+
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
+ )
Original file line number Diff line number Diff line change
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" )
Original file line number Diff line number Diff line change
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
+ }
You can’t perform that action at this time.
0 commit comments