Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON schema validation proposal #5852

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/CI-unixish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ jobs:

- name: Check syntax with NONNEG
run: |
ls lib/*.cpp | xargs -n 1 -P $(nproc) g++ -fsyntax-only -std=c++0x -Ilib -Iexternals -Iexternals/picojson -Iexternals/simplecpp -Iexternals/tinyxml2 -DNONNEG
ls lib/*.cpp | xargs -n 1 -P $(nproc) g++ -fsyntax-only -std=c++0x -Ilib -Iexternals -Iexternals/picojson -Iexternals/valijson -Iexternals/simplecpp -Iexternals/tinyxml2 -DNONNEG

build_qmake:

Expand Down Expand Up @@ -367,6 +367,7 @@ jobs:
python3 -m pip install pip --upgrade
python3 -m pip install pytest
python3 -m pip install pytest-timeout
python3 -m pip install jsonschema

- name: Build cppcheck
run: |
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/CI-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ jobs:
python -m pip install pytest || exit /b !errorlevel!
python -m pip install pytest-custom_exit_code || exit /b !errorlevel!
python -m pip install pytest-timeout || exit /b !errorlevel!
python -m pip install jsonschema || exit /b !errorlevel!

- name: Run CMake
if: false # TODO: enable
Expand Down Expand Up @@ -173,6 +174,7 @@ jobs:
python -m pytest -Werror --strict-markers -vv test-inline-suppress.py || exit /b !errorlevel!
python -m pytest -Werror --strict-markers -vv test-more-projects.py || exit /b !errorlevel!
python -m pytest -Werror --strict-markers -vv test-other.py || exit /b !errorlevel!
python -m pytest -Werror --strict-markers -vv test-json.py || exit /b !errorlevel!
python -m pytest -Werror --strict-markers -vv test-proj2.py || exit /b !errorlevel!
python -m pytest -Werror --strict-markers -vv test-suppress-syntaxError.py || exit /b !errorlevel!

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/asan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ jobs:
python3 -m pip install pip --upgrade
python3 -m pip install pytest
python3 -m pip install pytest-timeout
python3 -m pip install jsonschema

# TODO: disable all warnings
- name: CMake
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/tsan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ jobs:
python3 -m pip install pip --upgrade
python3 -m pip install pytest
python3 -m pip install pytest-timeout
python3 -m pip install jsonschema

- name: CMake
run: |
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ubsan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ jobs:
python3 -m pip install pip --upgrade
python3 -m pip install pytest
python3 -m pip install pytest-timeout
python3 -m pip install jsonschema

# TODO: disable warnings
- name: CMake
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ ifndef PREFIX
endif

ifndef INCLUDE_FOR_LIB
INCLUDE_FOR_LIB=-Ilib -isystem externals -isystem externals/picojson -isystem externals/simplecpp -isystem externals/tinyxml2
INCLUDE_FOR_LIB=-Ilib -isystem externals -isystem externals/picojson -isystem externals/valijson -isystem externals/simplecpp -isystem externals/tinyxml2
endif

ifndef INCLUDE_FOR_CLI
Expand Down
141 changes: 141 additions & 0 deletions addons/addon-namingng-config.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://cppcheck.com/addon.schema.json",
"title": "Addon namingng config",
"description": "namingng configuration format",
"type": "object",
"properties": {
"RE_FILE": {
"description": "Patterns for filenames",
"$ref": "#/definitions/re_list_or_none"
},
"RE_NAMESPACE": {
"description": "Patterns for namespaces",
"$ref": "#/definitions/re_list_or_dict_or_none"
},
"RE_VARNAME": {
"description": "Patterns for variable names",
"$ref": "#/definitions/re_list_or_dict_or_none"
},
"RE_PRIVATE_MEMBER_VARIABLE": {
"description": "Patterns for private member variable names",
"$ref": "#/definitions/re_list_or_dict_or_none"
},
"RE_PUBLIC_MEMBER_VARIABLE": {
"description": "Patterns for public member variable names",
"$ref": "#/definitions/re_list_or_dict_or_none"
},
"RE_GLOBAL_VARNAME": {
"description": "Patterns for global variable names",
"$ref": "#/definitions/re_list_or_dict_or_none"
},
"RE_FUNCTIONNAME": {
"description": "Patterns for function names",
"$ref": "#/definitions/re_list_or_dict_or_none"
},
"RE_CLASS_NAME": {
"description": "Patterns for class names",
"$ref": "#/definitions/re_list_or_dict_or_none"
},
"var_prefixes": {
"description": "Variable prefixes per type",
"$ref": "#/definitions/prefix_dict"
},
"function_prefixes": {
"description": "Variable prefixes per return type",
"$ref": "#/definitions/prefix_dict"
},
"skip_one_char_variables": {
"description": "Whether to ignore one-character local variables (default: false)",
"type":"boolean"
},
"include_guard": {
"properties": {
"input": {
"description": "What to take as input for the include guard name (default: path)",
"enum": ["path","basename"]
},
"case": {
"description": "What case the include guard name is in (default: upper)",
"enum": ["upper","lower","keep"]
},
"prefix": {
"description": "Include guard prefix",
"type": "string"
},
"suffix": {
"description": "Include guard suffix",
"type": "string"
},
"max_linenr": {
"description": "Don't consider include guards found after this line number (default: 5)",
"type": "number"
},
"required": {
"description": "Whether include guards are required for include files (default: true)",
"type": "boolean"
},
"RE_HEADERFILE": {
"description": "Pattern used to determine whether a file is an include file (default: relative paths ending in .h)",
"$ref": "#/definitions/python_re"
}
},
"additionalProperties": false
}
},
"additionalProperties": false,
"required": [],
"definitions": {
"re_list_or_dict_or_none":{
"anyOf": [
{ "$ref": "#/definitions/re_list" },
{ "$ref": "#/definitions/re_dict" },
{ "type": "null" }
]
},
"re_list_or_none":{
"anyOf": [
{ "$ref": "#/definitions/re_list" },
{ "type": "null" }
]
},
"re_list": {
"type": "array",
"items": {
"$ref": "#/definitions/python_re"
},
"minItems":1
},
"re_dict": {
"type": "object",
"propertyNames": {
"$ref": "#/definitions/python_re"
},
"patternProperties": {
"": {
"type": "array",
"prefixItems": [
{ "type": "boolean" },
{ "type": "string" }
],
"minItems": 2,
"unevaluatedItems": false
}
}
},
"prefix_dict": {
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9_*&]+$": {
"type":"string",
"pattern": "^[a-zA-Z0-9_]+$"
}
},
"additionalProperties": false
},
"python_re": {
"type": "string",
"format": "python_re"
}
}
}
35 changes: 35 additions & 0 deletions addons/addon.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://cppcheck.com/addon.schema.json",
"title": "Addon",
"description": "Cppcheck addon format",
"type": "object",
"properties": {
"script": {
"description": "The Python script containing the addon code.",
"type": "string"
},
"args": {
"description": "Arguments to pass to the script.",
"type": "array",
"items": {
"type": "string"
},
"minItems": 1
},
"python": {
"description": "The full path of the Python interpreter to use.",
"type": "string"
},
"ctu": {
"description": "Whether the addon is instructed to perform analysis across multiple translation units.",
"type": "boolean"
},
"executable": {
"description": "Override default 'python <script>' invocation [deprecated].",
"type": "string"
}
},
"additionalProperties": false,
"required": ["script"]
}
5 changes: 4 additions & 1 deletion addons/namingng.config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"RE_VARNAME": ["[a-z]*[a-zA-Z0-9_]*\\Z"],
"RE_VARNAME": {
"[a-z][a-zA-Z0-9_]*\\Z":[false,"must start with a-z"],
".*new":[true,"may not contain 'new'"]
},
"RE_PRIVATE_MEMBER_VARIABLE": null,
"RE_FUNCTIONNAME": ["[a-z0-9A-Z]*\\Z"],
"include_guard": {
Expand Down
92 changes: 26 additions & 66 deletions addons/namingng.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
import re
import argparse
import json
import jsonschema

config_schema = os.path.join(os.path.dirname(sys.argv[0]),'addon-namingng-config.schema.json')

# Auxiliary class
class DataStruct:
Expand All @@ -55,44 +58,26 @@ def configError(error,fatal=True):
if fatal:
sys.exit(1)

def validateConfigREs(list_or_dict,json_key):
have_error = False
for item in list_or_dict:
try:
re.compile(item)
except re.error as err:
configError("item '%s' of '%s' is not a valid regular expression: %s"%(item,json_key,err),fatal=False)
have_error = True
continue
if not isinstance(list_or_dict,dict):
continue
# item is actually a dict key; check value
value = list_or_dict[item]
if (not isinstance(value,list) or len(value) != 2
or not isinstance(value[0],bool) or not isinstance(value[1],str)):
configError("item '%s' of '%s' must be an array [bool,string]"%(item,json_key),fatal=False)
have_error = True

return have_error

def loadConfig(configfile):
if not os.path.exists(configfile):
configError("cannot find config file '%s'"%configfile)

try:
what = 'config file'
with open(configfile) as fh:
data = json.load(fh)
what = 'JSON schema'
with open(config_schema) as fh:
schema = json.load(fh)
except json.JSONDecodeError as e:
configError("error parsing config file as JSON at line %d: %s"%(e.lineno,e.msg))
configError("error parsing %s as JSON at line %d: %s"%(what,e.lineno,e.msg))
except Exception as e:
configError("error opening config file '%s': %s"%(configfile,e))

if not isinstance(data, dict):
configError('config file must contain a JSON object at the top level')
configError("error opening %s '%s': %s"%(what,configfile,e))

# All errors are emitted before bailing out, to make the unit test more
# effective.
have_error = False
try:
jsonschema.validate(instance=data,schema=schema)
except Exception as e:
configError("error validating config JSON: %s"%(e))

# Put config items in a class, so that settings can be accessed using
# config.feature
Expand All @@ -101,54 +86,29 @@ class Config:
config = Config()

mapping = {
'file': ('RE_FILE', (list,)),
'namespace': ('RE_NAMESPACE', (list,dict)),
'include_guard': ('include_guard', (dict,)),
'variable': ('RE_VARNAME', (list,dict)),
'variable_prefixes': ('var_prefixes', (dict,), {}),
'private_member': ('RE_PRIVATE_MEMBER_VARIABLE', (list,dict)),
'public_member': ('RE_PUBLIC_MEMBER_VARIABLE', (list,dict)),
'global_variable': ('RE_GLOBAL_VARNAME', (list,dict)),
'function_name': ('RE_FUNCTIONNAME', (list,dict)),
'function_prefixes': ('function_prefixes', (dict,), {}),
'class_name': ('RE_CLASS_NAME', (list,dict)),
'skip_one_char_variables': ('skip_one_char_variables', (bool,)),
'file': ('RE_FILE',),
'namespace': ('RE_NAMESPACE',),
'include_guard': ('include_guard',),
'variable': ('RE_VARNAME',),
'variable_prefixes': ('var_prefixes', {}),
'private_member': ('RE_PRIVATE_MEMBER_VARIABLE',),
'public_member': ('RE_PUBLIC_MEMBER_VARIABLE',),
'global_variable': ('RE_GLOBAL_VARNAME',),
'function_name': ('RE_FUNCTIONNAME',),
'function_prefixes': ('function_prefixes', {}),
'class_name': ('RE_CLASS_NAME',),
'skip_one_char_variables': ('skip_one_char_variables',),
}

# parse defined keys and store as members of config object
for key,opts in mapping.items():
json_key = opts[0]
req_type = opts[1]
default = None if len(opts)<3 else opts[2]

default = None if len(opts)<3 else opts[1]
value = data.pop(json_key,default)
if value is not None and type(value) not in req_type:
req_typename = ' or '.join([tp.__name__ for tp in req_type])
got_typename = type(value).__name__
configError('%s must be %s (not %s), or not set'%(json_key,req_typename,got_typename),fatal=False)
have_error = True
continue

# type list implies that this is either a list of REs or a dict with RE keys
if list in req_type and value is not None:
re_error = validateConfigREs(value,json_key)
if re_error:
have_error = True

setattr(config,key,value)

# check remaining keys, only accept underscore-prefixed comments
for key,value in data.items():
if key == '' or key[0] != '_':
configError("unknown config key '%s'"%key,fatal=False)
have_error = True

if have_error:
sys.exit(1)

return config


def evalExpr(conf, exp, mockToken, msgType):
report_as_error = False
msg = msgType + ' ' + mockToken.str + ' violates naming convention'
Expand Down
2 changes: 1 addition & 1 deletion createrelease
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
#
# self check, fix critical issues:
# make clean && make CXXFLAGS=-O2 MATCHCOMPILER=yes -j4
# ./cppcheck -D__CPPCHECK__ -D__GNUC__ -DCHECK_INTERNAL -DHAVE_RULES --std=c++11 --library=cppcheck-lib --library=qt --enable=style --inconclusive --inline-suppr --suppress=bitwiseOnBoolean --suppress=shadowFunction --suppress=useStlAlgorithm --suppress=*:externals/picojson.h --suppress=functionConst --suppress=functionStatic --xml cli gui/*.cpp lib 2> selfcheck.xml
# ./cppcheck -D__CPPCHECK__ -D__GNUC__ -DCHECK_INTERNAL -DHAVE_RULES --std=c++11 --library=cppcheck-lib --library=qt --enable=style --inconclusive --inline-suppr --suppress=bitwiseOnBoolean --suppress=shadowFunction --suppress=useStlAlgorithm --suppress=*:externals/picojson.h --suppress=*:externals/valijson/valijson_picojson_bundled.hpp --suppress=functionConst --suppress=functionStatic --xml cli gui/*.cpp lib 2> selfcheck.xml
#
# Generate lib/checkers.cpp (TODO the premium checkers should not be statically coded)
# cd ~/cppchecksolutions/cppcheck && python3 tools/get_checkers.py > lib/checkers.cpp
Expand Down
Loading
Loading