-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Add qsscheck to CI pipeline and improve .travis.yml config #2375
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
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
2c00901
scripts: Add qsscheck.py script
Holzhaus 151fe9e
.travis.yml: Run qsscheck.py on Linux CI builds
Holzhaus 66c5ab4
.travis.yml: Cleanup travis config and run linter as separate job
Holzhaus dceea54
.travis.yml: Add job names
Holzhaus 44f970e
.travis.yml: Fix setting global env variables
Holzhaus 4acbcfb
.travis.yml: Remove unused before_script for osx builds
Holzhaus deb4c23
.travis.yml: Print QTDIR on osx builds
Holzhaus 65724d5
.travis.yml: Use python 3.7 by default on OSX
Holzhaus 91c7064
.travis.yml: Replace "matrix" alias with more descriptive "jobs"
Holzhaus 4a075fb
Revert ".travis.yml: Use python 3.7 by default on OSX"
Holzhaus 35e4801
.travis.yml: Only install python3 on qsscheck
Holzhaus 779fd9f
scripts/qsscheck: Pre-compile regexes
Holzhaus d85fadd
scripts/qsscheck: Reduce indention depth using continue statement
Holzhaus 6360f9f
scripts/qsscheck: Only run glob-matching for actual glob objectnames
Holzhaus af9f350
scripts/qsscheck: Improve qscheck script and add -p parameter
Holzhaus b723e5f
scripts/qsscheck: Move core logic from main() into separate function
Holzhaus 8e29cf2
scripts/qsscheck: Fix line exceeding maximum line lengths
Holzhaus f60bdd8
scripts/qsscheck: Add docstrings for all functions
Holzhaus 9bd7d84
Merge branch 'master' of https://github.com/mixxxdj/mixxx into qsscheck
Holzhaus 7ba04af
Merge branch 'master' of https://github.com/mixxxdj/mixxx into qsscheck
Holzhaus File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| #!/usr/bin/env python3 | ||
| # -*- coding: utf-8 -*- | ||
| import argparse | ||
| import fnmatch | ||
| import os | ||
| import os.path | ||
| import re | ||
| import sys | ||
| import tinycss.css21 | ||
| import PyQt5.QtWidgets | ||
|
|
||
|
|
||
| RE_CPP_CLASSNAME = re.compile(r'^\s*class\s+([\w_]+)') | ||
| RE_CPP_OBJNAME = re.compile(r'setObjectName\(.*"([^"]+)"') | ||
| RE_UI_OBJNAME = re.compile(r'<widget[^>]+name="([^"]+)"') | ||
| RE_XML_OBJNAME = re.compile(r'<ObjectName>(.*)</ObjectName>') | ||
| RE_XML_OBJNAME_SETVAR = re.compile( | ||
| r'<SetVariable\s+name="ObjectName">(.*)</SetVariable>') | ||
| RE_CLASSNAME = re.compile(r'^[A-Z]\w+$') | ||
| RE_OBJNAME_VARTAG = re.compile(r'<.*>') | ||
|
|
||
|
|
||
| def get_skins(path): | ||
| """Yields (skin_name, skin_path) tuples for each skin directory in path.""" | ||
| for entry in os.scandir(path): | ||
| if entry.is_dir(): | ||
| yield entry.name, os.path.join(path, entry.name) | ||
|
|
||
|
|
||
| def get_global_names(mixxx_path): | ||
| """Returns 2 sets with all class and object names in the Mixx codebase.""" | ||
| classnames = set() | ||
| objectnames = set() | ||
| for root, dirs, fnames in os.walk(os.path.join(mixxx_path, 'src')): | ||
| for fname in fnames: | ||
| ext = os.path.splitext(fname)[1] | ||
| if ext in ('.h', '.cpp'): | ||
| fpath = os.path.join(root, fname) | ||
| with open(fpath, mode='r') as f: | ||
| for line in f: | ||
| classnames.update(set(RE_CPP_CLASSNAME.findall(line))) | ||
| objectnames.update(set(RE_CPP_OBJNAME.findall(line))) | ||
| elif ext == '.ui': | ||
| fpath = os.path.join(root, fname) | ||
| with open(fpath, mode='r') as f: | ||
| objectnames.update(set(RE_UI_OBJNAME.findall(f.read()))) | ||
| return classnames, objectnames | ||
|
|
||
|
|
||
| def get_skin_objectnames(skin_path): | ||
| """ | ||
| Yields all object names in the skin_path. | ||
|
|
||
| Note the names may contain one or more <Variable name="x"> tags, so it's | ||
| not enough to check if a name CSS object name is in this list using "in". | ||
| """ | ||
| for root, dirs, fnames in os.walk(skin_path): | ||
| for fname in fnames: | ||
| if os.path.splitext(fname)[1] != '.xml': | ||
| continue | ||
|
|
||
| fpath = os.path.join(root, fname) | ||
| with open(fpath, mode='r') as f: | ||
| for line in f: | ||
| yield from RE_XML_OBJNAME.findall(line) | ||
| yield from RE_XML_OBJNAME_SETVAR.findall(line) | ||
|
|
||
|
|
||
| def get_skin_stylesheets(skin_path): | ||
| """Yields (qss_path, stylesheet) tuples for each qss file in skin_path).""" | ||
| cssparser = tinycss.css21.CSS21Parser() | ||
| for filename in os.listdir(skin_path): | ||
| if os.path.splitext(filename)[1] != '.qss': | ||
| continue | ||
| qss_path = os.path.join(skin_path, filename) | ||
| stylesheet = cssparser.parse_stylesheet_file(qss_path) | ||
| yield qss_path, stylesheet | ||
|
|
||
|
|
||
| def check_stylesheet(stylesheet, classnames, objectnames, objectnames_fuzzy): | ||
| """Yields (token, problem) tuples for each problem found in stylesheet.""" | ||
| for rule in stylesheet.rules: | ||
| if not isinstance(rule, tinycss.css21.RuleSet): | ||
| continue | ||
| for token in rule.selector: | ||
| if token.type == 'IDENT': | ||
| if not RE_CLASSNAME.match(token.value): | ||
| continue | ||
| if token.value in classnames: | ||
| continue | ||
| if token.value in dir(PyQt5.QtWidgets): | ||
| continue | ||
| yield (token, 'Unknown widget class "%s"' % token.value) | ||
|
|
||
| elif token.type == 'HASH': | ||
| value = token.value[1:] | ||
| if value in objectnames: | ||
| continue | ||
|
|
||
| if any(fnmatch.fnmatchcase(value, objname) | ||
| for objname in objectnames_fuzzy): | ||
| continue | ||
|
|
||
| yield (token, 'Unknown object name "%s"' % token.value) | ||
|
|
||
|
|
||
| def check_skins(mixxx_path, skins, ignore_patterns=()): | ||
| """ | ||
| Yields error messages for skins using class/object names from mixxx_path. | ||
|
|
||
| By providing a list of ignore_patterns, you can ignore certain class or | ||
| object names (e.g. #Test, #*Debug). | ||
| """ | ||
| classnames, objectnames = get_global_names(mixxx_path) | ||
| for skin_name, skin_path in sorted(skins): | ||
| # If the skin objectname is something like 'Deck<Variable name="i">', | ||
| # then replace it with 'Deck*' and use glob-like matching | ||
| skin_objectnames = objectnames.copy() | ||
| skin_objectnames_fuzzy = set() | ||
| for objname in get_skin_objectnames(skin_path): | ||
| new_objname = RE_OBJNAME_VARTAG.sub('*', objname) | ||
| if '*' in new_objname: | ||
| skin_objectnames_fuzzy.add(new_objname) | ||
| else: | ||
| skin_objectnames.add(new_objname) | ||
|
|
||
| for qss_path, stylesheet in get_skin_stylesheets(skin_path): | ||
| for error in stylesheet.errors: | ||
| yield '%s:%d:%d: %s - %s' % ( | ||
| qss_path, error.line, error.column, | ||
| error.__class__.__name__, error.reason, | ||
| ) | ||
| for token, message in check_stylesheet( | ||
| stylesheet, classnames, | ||
| skin_objectnames, skin_objectnames_fuzzy): | ||
| if any(fnmatch.fnmatchcase(token.value, pattern) | ||
| for pattern in ignore_patterns): | ||
| continue | ||
| yield '%s:%d:%d: %s' % ( | ||
| qss_path, token.line, token.column, message, | ||
| ) | ||
|
|
||
|
|
||
| def main(argv=None): | ||
| """Main method for handling command line arguments.""" | ||
| parser = argparse.ArgumentParser('qsscheck', description='Check Mixxx QSS ' | ||
| 'stylesheets for non-existing ' | ||
| 'object/class names') | ||
| parser.add_argument('-p', '--extra-skins-path', | ||
| help='Additonal skin path, to check (.e.g. ' | ||
| '"~/.mixxx/skins")') | ||
| parser.add_argument('-s', '--skin', help='Only check skin with this name') | ||
| parser.add_argument('-i', '--ignore', default='', | ||
| help='Glob pattern of class/object names to ignore ' | ||
| '(e.g. "#Test*"), separated by commas') | ||
| parser.add_argument('mixxx_path', help='Path of Mixxx sources/git repo') | ||
| args = parser.parse_args(argv) | ||
|
|
||
| mixxx_path = args.mixxx_path | ||
|
|
||
| skins_path = os.path.join(mixxx_path, 'res', 'skins') | ||
| skins = set(get_skins(skins_path)) | ||
| if args.extra_skins_path: | ||
| skins.update(set(get_skins(args.extra_skins_path))) | ||
|
|
||
| if args.skin: | ||
| skins = set((name, path) for name, path in skins if name == args.skin) | ||
|
|
||
| if not skins: | ||
| print('No skins to check') | ||
| return 1 | ||
|
|
||
| status = 0 | ||
| for message in check_skins(mixxx_path, skins, args.ignore.split(',')): | ||
| print(message) | ||
| status = 2 | ||
| return status | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| sys.exit(main()) | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(see below comment -- here's another place where we can save a level of indentation)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How? We have an elseif case, too.