vxSDK/scripts/checkers/exposed_func.py

313 lines
9.5 KiB
Python

"""
exposed_func - checker for vxnorm
This file does not expose an explicite VxChecker object declaration to avoid
dependencies handling, you just need to provide:
======================= ===============================================
parse_file() Parse the source file
======================= ===============================================
"""
#---
# Private
#---
## checker rules functions
def _checker_rule_obj(name, checker, mfile, token):
""" mutual class/def checker
"""
if len(token) <= 1:
checker.notify(mfile.line, f"malformated {name} declaraction")
return ''
objname = token[1]
if (has_parenthesis := objname.find('(')) > 0:
objname = token[1][:has_parenthesis]
return objname
def _checker_rule_class(checker, mfile, token):
""" handle `class <>` formalism
"""
checker.select_rule('class')
return _checker_rule_obj('class', checker, mfile, token)
def _checker_rule_def(checker, mfile, token):
""" handle `def <>(<>)` formalism
"""
checker.select_rule('function')
return _checker_rule_obj('def', checker, mfile, token)
def _checker_rule_from_import(_, __, token):
""" handle `from <>` formalism
"""
return token[0]
def _checker_rule_context(checker, mfile, token):
""" handle special context indication formalism
"""
checker.select_rule('context')
if not (token := mfile.readline()):
checker.notify(mfile.line, 'malformated context switch')
return ''
if token not in ['# Public\n', '# Internals\n']:
return ''
context = token[2:-1]
if (token := mfile.readline()) not in ['#\n', '#---\n']:
checker.notify(
mfile.line,
"context switch should have empty line between context and desc."
)
return context
def _checker_rule_all(checker, mfile, line_token):
""" handle __all__ formalism
"""
checker.select_rule('__all__')
if line_token != ['__all__', '=', '[']:
checker.notify(mfile.line, 'malformated __all__ declaration')
return ''
funclist = []
for line in mfile.getlines():
for token in line.split():
if token == ']':
return funclist
if token[0] not in ['\'', '"']:
checker.notify(mfile.line, 'malformated function declaraction')
continue
if token[0] == '"':
checker.notify(
mfile.line,
'function declaraction should start with single quote'
)
funclist.append(token[1: (-1 + (token[-1] != ',')) - 1])
return []
## checker layout functions
def _checker_layout_import(checker, info, token):
""" check from / import position
@rules
> any import must be placed before __all__ declaration
> any import must be performed before any active context
> 'import' must be place before 'from'
"""
if info['dall']:
checker.notify(
token['line'],
'__all__ declaration should be placed after any import'
)
if token['tag'] == 'import':
if info['dfrom']:
checker.notify(
token['line'],
'\'import\' should be placed before any \'from\''
)
else:
info['dfrom'] = True
def _checker_layout_all(checker, info, token):
""" check __all__ position
@rules
> '__all__' must be declared only one time
"""
if info['dall']:
checker.notify(
token['line'],
'multiple definition of __all__ declaration'
)
info['sym_exposed'] += token['data']
info['dall'] = True
def _checker_layout_context(checker, info, token):
""" check context layout
@rules
> 'Public' layout must be placed after 'Private'
> multiple context switch not allowed (Privte -> Public or Public only)
"""
if info['context']:
if info['context'] == token['data']:
checker.notify(
token['line'],
f"multiple '{token['data']}' context definition"
)
return
if info['context'] == 'Public':
checker.notify(
token['line'],
'switching between Public -> Private context'
)
return
info['context'] = token['data']
def _checker_layout_class(checker, info, token):
""" check class definition
@rules
> [~] handle default context if no one is specified
> if 'Public' context -> must not start with '_'
> if 'Private' context -> must start with '_'
> name must start with capital letter
"""
if not info['context']:
checker.notify(
token['line'],
'missing explicit context selection, switch to \'Public\''
)
info['context'] = 'Public'
if info['context'] == 'Public':
if token['data'][0] == '_':
checker.notify(
token['line'],
'public class must not start with underscore'
)
return
if not token['data'][0].isupper():
checker.notify(
token['line'],
'class must start its name with capital letter'
)
info['symbols'].append(token['data'])
else:
if token['data'][0] != '_':
checker.notify(
token['line'],
'private class must start with underscore'
)
return
if not token['data'][1].isupper():
checker.notify(
token['line'],
'class must start its name with capital letter'
)
def _checker_layout_def(checker, info, token):
""" check function name
@rules
> no capital letters
> no numeric caracters
> [~] handle default context if no one is specified
> in Private -> must start with one underscore
> in Public -> must start with the modulename and no capital letter
"""
if any(char.isupper() for char in token['data']):
checker.notify(
token['line'],
'function must no contain capital letters'
)
if not info['context']:
checker.notify(
token['line'],
'missing explicit context selection, switch to \'Public\''
)
info['context'] = 'Public'
if info['context'] != 'Public':
if token['data'][0] != '_':
checker.notify(
token['line'],
'private function must start with underscore'
)
return
if token['data'][1] == '_':
checker.notify(
token['line'],
'private function must only have one underscore'
)
else:
info['symbols'].append(token['data'])
if not info['modname'] or info['special']:
return
if token['data'].find(info['modname']) != 0:
checker.notify(
token['line'],
'public function must start with the module name '
f"({info['modname']})"
)
#---
# Public
#---
def parse_file(checker, mfile, pathinfo):
""" parse the mapped file
The file is mapped using mmap() and seeked through offset 0 to avoid too
many I/O operations with classical file primitive.
@args
> checker (VxChecker) - current checker instance for this file
> mfile (mmap) - mmap instance of the file, seeked at 0
> pathname (str) - file pathname
@return
> Nothing
"""
table = [
('__all__', _checker_rule_all, _checker_layout_all),
('class', _checker_rule_class, _checker_layout_class),
('def', _checker_rule_def, _checker_layout_def),
('#---', _checker_rule_context, _checker_layout_context),
('from', _checker_rule_from_import, _checker_layout_import),
('import', _checker_rule_from_import, _checker_layout_import),
]
layout = []
checker.select_rule('expofunc')
for line in mfile.getlines():
if line[0].isspace():
continue
if not (token := line.split()):
continue
for keyword in table:
if token[0] != keyword[0]:
continue
line = mfile.line
if not (data := keyword[1](checker, mfile, token)):
continue
layout.append({
'tag' : keyword[0],
'data' : data,
'line' : line
})
modname = pathinfo['filename'][:-3]
if pathinfo['filename'] == '__init__.py':
modname = ''
# special behaviour for CLI exposition file
info = {
'modname' : modname,
'dall' : False,
'dfrom' : False,
'sym_exposed' : [],
'context' : '',
'symbols' : [],
'special' : 'cli' in pathinfo['dirs']
}
find = False
checker.select_rule('layout')
for token in layout:
find = False
for keyword in table:
if token['tag'] != keyword[0]:
continue
keyword[2](checker, info, token)
find = True
break
if not find:
checker.notify('internal', f"unknown token '{token['tag']}'")
if not info['symbols'] or info['special']:
return
if info['symbols'] != info['sym_exposed']:
text = '__all__ = [\n '
text += ',\n '.join(info['symbols'])
checker.notify(
0,
f"mismatch exposed function, you should use:\n{text}\n]"
)