Skip to content

Commit

Permalink
Merge pull request #19 from cnescatlab/dev
Browse files Browse the repository at this point in the history
Release 7.0.0
  • Loading branch information
louisjdmartin committed Apr 23, 2024
2 parents aead744 + 113e294 commit eeef7b9
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 57 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ cnes-pylint-extension checks the following metrics :
- Version 4.0 - compatible pylint 2.1.1
- Version 5.0 - compatible pylint >=2.5.0,<2.12.0
- Version 6.0 - compatible pylint >=2.12,<3.0.0
- **warning**: At 6.0.0 release, latest pylint was 2.13.5. If you encounter issue with pylint>2.13.5 and <3.0.0 please open an issue.
- Version 7.0 - compatible pylint >=3.0.0,<4.0.0
- **warning**: At 7.0.0 release, latest pylint was 3.0.3. If you encounter issue with pylint>3.0.3 and <4.0.0 please open an issue.

# To use these checkers:

Expand All @@ -39,7 +40,7 @@ cnes-pylint-extension checks the following metrics :

### Install Pylint

`pip install pylint==2.13.5`
`pip install "pylint>=3.0.0,<4.0.0"`

### Install CNES Pylint extension checkers

Expand Down
113 changes: 62 additions & 51 deletions checkers/cnes_checker/cnes_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@
import astroid
from astroid.exceptions import InferenceError
from pylint.extensions import docparams
from pylint.interfaces import IAstroidChecker, ITokenChecker
from pylint.checkers import BaseChecker, BaseTokenChecker
from pylint.checkers.utils import check_messages
from pylint.checkers import utils
from pylint.checkers.raw_metrics import get_type
from pylint.constants import WarningScope
import tokenize
Expand All @@ -33,8 +32,6 @@
class DesignChecker(BaseChecker):
"""Checks for multiple exit statements in loops"""

__implements__ = (IAstroidChecker,)

name = 'design'
msgs = {'R5101': ('More than one exit statement for this loop',
'multiple-exit-statements',
Expand Down Expand Up @@ -73,12 +70,13 @@ class DesignChecker(BaseChecker):

def __init__(self, linter=None):
BaseChecker.__init__(self, linter)
self.config = linter.config
self._exit_statements = []

def visit_for(self, node):
self._exit_statements.append(0)

@check_messages('use-context-manager')
@utils.only_required_for_messages('use-context-manager')
def visit_attribute(self, node):
try:
for infer in node.infer():
Expand All @@ -94,7 +92,7 @@ def visit_attribute(self, node):
except InferenceError:
pass

@check_messages('use-context-manager')
@utils.only_required_for_messages('use-context-manager')
def visit_call(self, node):
try:
for funcdef in node.func.infer():
Expand All @@ -108,7 +106,7 @@ def visit_call(self, node):
except InferenceError:
pass

@check_messages('bad-exit-condition')
@utils.only_required_for_messages('bad-exit-condition')
def visit_while(self, node):
self._exit_statements.append(0)
comparisons = None
Expand All @@ -124,7 +122,7 @@ def visit_while(self, node):
if ops[0] in ('!=', '=='):
self.add_message('bad-exit-condition', node=node)

@check_messages('multiple-exit-statements')
@utils.only_required_for_messages('multiple-exit-statements')
def leave_for(self, node):
if self._exit_statements[-1] > 1:
self.add_message('multiple-exit-statements', node=node)
Expand All @@ -137,13 +135,14 @@ def visit_return(self, node):
visit_break = visit_return
leave_while = leave_for

@check_messages('too-many-decorators')
@utils.only_required_for_messages('too-many-decorators')
def visit_functiondef(self, node):
max_decorators = getattr(self.config, 'max_decorators', self.options[0][1]['default'])
if node.decorators:
if len(node.decorators.nodes) > self.config.max_decorators:
if len(node.decorators.nodes) > max_decorators:
self.add_message('too-many-decorators', node=node,
args=(len(node.decorators.nodes),
self.config.max_decorators))
max_decorators))
for child in node.nodes_of_class(astroid.Call):
try:
for funcdef in child.func.infer():
Expand All @@ -153,7 +152,7 @@ def visit_functiondef(self, node):
except:
continue

@check_messages('builtin-name-used')
@utils.only_required_for_messages('builtin-name-used')
def visit_classdef(self, node):
for name, item in node.instance_attrs.items():
self._check_node_name(node, item[0], name)
Expand All @@ -180,8 +179,6 @@ def _check_node_name(self, class_node, item, name):
class CommentMetricsChecker(BaseTokenChecker):
"""Checks the ratio comments+docstrings/code lines by module and by function
"""

__implements__ = (ITokenChecker, IAstroidChecker)

# Theses values are hardcoded in pylint (and have changed in pylint 2.12)
# We can't get them directly from the pylint lib :(
Expand All @@ -199,6 +196,7 @@ class CommentMetricsChecker(BaseTokenChecker):
{'scope': WarningScope.NODE}
),
}

options = (('min-func-comments-ratio',
{'default': 30, 'type': 'int', 'metavar': '<int>',
'help': 'Minimum ratio (comments+docstrings)/code_lines for a '
Expand All @@ -215,6 +213,7 @@ class CommentMetricsChecker(BaseTokenChecker):

def __init__(self, linter):
BaseTokenChecker.__init__(self, linter)
self.config = linter.config
self._reset()

def _reset(self):
Expand All @@ -238,13 +237,14 @@ def process_tokens(self, tokens):
self._stats[start_line] = [line_type, lines_number]
tail = start_line

@check_messages('too-few-comments')
@utils.only_required_for_messages('too-few-comments')
def visit_functiondef(self, node):
min_func_comments_ratio = getattr(self.config, 'min_func_comments_ratio', self.options[0][1]['default'])
min_func_size_to_check_comments = getattr(self.config, 'min_func_size_to_check_comments', self.options[2][1]['default'])
nb_lines = node.tolineno - node.fromlineno
if nb_lines <= self.config.min_func_size_to_check_comments:
if nb_lines <= min_func_size_to_check_comments:
return
func_stats = dict.fromkeys(self.LINE_TYPES,
0)
func_stats = dict.fromkeys(self.LINE_TYPES, 0)
for line in sorted(self._stats):
if line > node.tolineno:
break
Expand All @@ -258,22 +258,22 @@ def visit_functiondef(self, node):
return
ratio = ((func_stats[self.LINE_TYPE_COMMENT] + func_stats[self.LINE_TYPE_DOCSTRING])
/ float(func_stats[self.LINE_TYPE_CODE]) * 100)
if ratio < self.config.min_func_comments_ratio:
if ratio < min_func_comments_ratio:
self.add_message('too-few-comments', node=node,
args=('%.2f' % ratio,
self.config.min_func_comments_ratio))
args=(f'{ratio:.2f}', min_func_comments_ratio))

@check_messages('too-few-comments')
@utils.only_required_for_messages('too-few-comments')
def visit_module(self, node):
min_module_comments_ratio = getattr(self.config, 'min_module_comments_ratio', self.options[1][1]['default'])
if self._global_stats[self.LINE_TYPE_CODE] <= 0:
return
ratio = ((self._global_stats[self.LINE_TYPE_COMMENT] +
self._global_stats[self.LINE_TYPE_DOCSTRING]) /
float(self._global_stats[self.LINE_TYPE_CODE]) * 100)
if ratio < self.config.min_module_comments_ratio:
if ratio < min_module_comments_ratio:
self.add_message('too-few-comments', node=node,
args=('%.2f' % ratio,
self.config.min_module_comments_ratio))
args=(f'{ratio:.2f}', min_module_comments_ratio))


def leave_module(self, node):
self._reset()
Expand Down Expand Up @@ -346,7 +346,7 @@ def visitFunctionDef(self, node):
pathnode = self._append_node(node)
self.tail = pathnode
self.dispatch_list(node.body)
bottom = "%s" % self._bottom_counter
bottom = f"{self._bottom_counter}"
self._bottom_counter += 1
self.graph.connect(self.tail, bottom)
self.graph.connect(node, bottom)
Expand All @@ -355,7 +355,7 @@ def visitFunctionDef(self, node):
self.graph = PathGraph(node)
self.tail = node
self.dispatch_list(node.body)
self.graphs["%s%s" % (self.classname, node.name)] = self.graph
self.graphs[f"{self.classname}{node.name}"] = self.graph
self.reset()

def visitClassDef(self, node):
Expand All @@ -373,17 +373,17 @@ def visitSimpleStatement(self, node):
visitExpr = visitSimpleStatement

def visitIf(self, node):
name = "If %d" % node.lineno
name = f"If {node.lineno}"
self._subgraph(node, name)

def visitLoop(self, node):
name = "Loop %d" % node.lineno
name = f"Loop {node.lineno}"
self._subgraph(node, name)

visitFor = visitWhile = visitLoop

def visitTryExcept(self, node):
name = "TryExcept %d" % node.lineno
name = f"TryExcept {node.lineno}"
self._subgraph(node, name, extra_blocks=node.handlers)

visitTry = visitTryExcept
Expand All @@ -405,7 +405,7 @@ def _subgraph(self, node, name, extra_blocks=()):
# global loop
self.graph = PathGraph(node)
self._subgraph_parse(node, extra_blocks)
self.graphs["%s%s" % (self.classname, name)] = self.graph
self.graphs[f"{self.classname}{name}"] = self.graph
self.reset()
else:
self._append_node(node)
Expand All @@ -428,7 +428,7 @@ def _subgraph_parse(self, node, extra_blocks):
else:
loose_ends.append(node)
if node:
bottom = "%s" % self._bottom_counter
bottom = f"{self._bottom_counter}"
self._bottom_counter += 1
for le in loose_ends:
self.graph.connect(le, bottom)
Expand All @@ -438,8 +438,6 @@ def _subgraph_parse(self, node, extra_blocks):
class McCabeChecker(BaseChecker):
"""Checks for functions or methods having a high McCabe number"""

__implements__ = (IAstroidChecker,)

name = 'mccabe'
msgs = {'R5301': ('Too high cyclomatic complexity (mccabe %d/%d)',
'too-high-complexity',
Expand All @@ -462,30 +460,33 @@ class McCabeChecker(BaseChecker):

def __init__(self, linter=None):
BaseChecker.__init__(self, linter)
self.config = linter.config
self.simplified_mccabe_number = []

@check_messages('too-high-complexity')
@utils.only_required_for_messages('too-high-complexity')
def visit_module(self, node):
max_mccabe_number = getattr(self.config, 'max_mccabe_number', self.options[0][1]['default'])
visitor = McCabeASTVisitor()
for child in node.body:
visitor.preorder(child, visitor)
for graph in visitor.graphs.values():
complexity = graph.complexity()
if complexity > self.config.max_mccabe_number:
if complexity > max_mccabe_number:
self.add_message('too-high-complexity', node=graph.root,
args=(complexity,
self.config.max_mccabe_number))
max_mccabe_number))

def visit_functiondef(self, node):
self.simplified_mccabe_number.append(0)

@check_messages('max-simplified-mccabe-number')
@utils.only_required_for_messages('max-simplified-mccabe-number')
def leave_functiondef(self, node):
max_simplified_mccabe_number = getattr(self.config, 'max_simplified_mccabe_number', self.options[1][1]['default'])
complexity = self.simplified_mccabe_number.pop()
if complexity > self.config.max_simplified_mccabe_number:
if complexity > max_simplified_mccabe_number:
self.add_message('too-high-complexity-simplified', node=node,
args=(complexity,
self.config.max_simplified_mccabe_number))
max_simplified_mccabe_number))

def visit_while(self, node):
if self.simplified_mccabe_number:
Expand Down Expand Up @@ -514,25 +515,31 @@ class SphinxDocChecker(docparams.DocstringParameterChecker):

regexp = {}
for field in ('author', 'version', 'date'):
regexp[field] = (re.compile(r':%s:' % field),
re.compile(r':%s: \S+' % field))
regexp[field] = (re.compile(fr':{field}:'),
re.compile(fr':{field}: \S+'))

@check_messages('malformed-docstring-field', 'missing-docstring-field')
@utils.only_required_for_messages('malformed-docstring-field', 'missing-docstring-field')
def visit_module(self, node):
if not hasattr(node, 'doc') :
return
if not node.doc:
return
for field, expr in self.regexp.values():
self._check_docstring_field(node, field, expr)

@check_messages('malformed-docstring-field', 'missing-docstring-field')
@utils.only_required_for_messages('malformed-docstring-field', 'missing-docstring-field')
def visit_classdef(self, node):
if not hasattr(node, 'doc') :
return
if not node.doc:
return
self._check_description_exists(node)

@check_messages('malformed-docstring-field', 'missing-docstring-field')
@utils.only_required_for_messages('malformed-docstring-field', 'missing-docstring-field')
def visit_functiondef(self, node):
super(SphinxDocChecker, self).visit_functiondef(node)
if not hasattr(node, 'doc') :
return
if not node.doc:
return
self._check_description_exists(node)
Expand All @@ -542,7 +549,7 @@ def _check_description_exists(self, node):
To do so, check the first line contains something
"""
if not node.doc:
if not hasattr(node, 'doc') :
return
doc_lines = [line.strip() for line in node.doc.splitlines() if line]
if not doc_lines or doc_lines[0].startswith(':'):
Expand All @@ -565,7 +572,6 @@ def _check_docstring_field(self, node, field, expr):
class ForbiddenUsageChecker(BaseChecker):
"""Checks for use of forbidden functions or variables"""

__implements__ = (IAstroidChecker,)
name = 'forbiddenusage'
msgs = {'R5401': ('Consider dropping use of sys.exit()',
'sys-exit-used',
Expand All @@ -576,6 +582,11 @@ class ForbiddenUsageChecker(BaseChecker):
'os-environ-used',
'Used when environment variables are accessed. A program'
'should not rely on its execution environment.'
'The project properties such as login, database access URL,'
'system properties, etc. could be managed using a properties'
'file (XML, YAML or JSON format). Python modules like'
'"configparser" (python version > 3.10.7) could be useful'
'to manage properties along the project development.'
),
'R5403': ('Consider using argparse module instead of sys.argv',
'sys-argv-used',
Expand Down Expand Up @@ -614,7 +625,7 @@ def visit_module(self, node):
if self._is_sys_exit_call(call):
self._authorized_exits.append(call)

@check_messages('sys-exit-used')
@utils.only_required_for_messages('sys-exit-used')
def visit_call(self, node):
self._check_os_environ_call(node)
if not self._is_sys_exit_call(node):
Expand All @@ -625,14 +636,14 @@ def visit_call(self, node):
return
self.add_message('sys-exit-used', node=node)

@check_messages('os-environ-used', 'sys-argv-used')
@utils.only_required_for_messages('os-environ-used', 'sys-argv-used')
def visit_attribute(self, node):
if self._check_access(node, ('os', os.name), 'environ', astroid.Dict):
self.add_message('os-environ-used', node=node, args='environ')
if self._check_access(node, ('sys',), 'argv', astroid.List):
self.add_message('sys-argv-used', node=node)

@check_messages('os-environ-used', 'sys-argv-used')
@utils.only_required_for_messages('os-environ-used', 'sys-argv-used')
def visit_name(self, node):
if self._check_access(node, ('os', os.name), 'environ', astroid.Dict,
False):
Expand Down Expand Up @@ -684,7 +695,7 @@ def _check_os_environ_call(self, node):
if (funcdef.name in ('putenv', 'getenv', 'unsetenv')
and funcdef.root().name in('os', os.name)):
self.add_message('os-environ-used', node=node,
args='%s()' % funcdef.name)
args=f"{funcdef.name}()")
return
except InferenceError:
pass
Expand Down
Loading

0 comments on commit eeef7b9

Please sign in to comment.