313 lines
9.5 KiB
Python
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]"
|
|
)
|