 165b814f5d
			
		
	
	
		165b814f5d
		
	
	
	
	
		
			
			this is necessary in order to get Furnace to build using CMake 4.0. you should do: git submodule deinit extern/portaudio
		
			
				
	
	
		
			298 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			298 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # PortAudio Repository Whitespace Linter
 | |
| #
 | |
| # Run this script from the root of the repository using the command:
 | |
| #   python pa_whitelint.py
 | |
| #
 | |
| # Check all source files for the following:
 | |
| #   1. Consistent line endings are used throughout each file.
 | |
| #   2. No tabs are present. Use spaces for indenting.
 | |
| #   3. Indenting: leading whitespace is usually a multiple of 4 spaces,
 | |
| #      with permissive exceptions for continuation lines.
 | |
| #   4. Lines have no trailing whitespace.
 | |
| #   5. No non-ASCII or weird control characters are present.
 | |
| #   6. End-of-line is present at end-of-file.
 | |
| #   7. No empty (or whitespace) lines at end-of-file.
 | |
| 
 | |
| 
 | |
| from pathlib import Path
 | |
| import re
 | |
| import sys
 | |
| 
 | |
| # Configuration:
 | |
| 
 | |
| # Check these file types:
 | |
| sourceFileTypes = ["*.c", "*.h", "*.cpp", "*.cxx", "*.hxx"]
 | |
| 
 | |
| # Scan these directories
 | |
| dirs = ["src", "include", "examples", "test", "qa"]
 | |
| 
 | |
| # Exclude files or directories with the following names:
 | |
| excludePathParts = [
 | |
|     "ASIOSDK",
 | |
|     "iasiothiscallresolver.cpp",
 | |
|     "iasiothiscallresolver.h",
 | |
|     "mingw-include",
 | |
| ]
 | |
| 
 | |
| indentSpaceCount = 4
 | |
| 
 | |
| verbose = True
 | |
| checkBadIndenting = True
 | |
| verboseBadIndenting = True
 | |
| 
 | |
| # (End configuration)
 | |
| 
 | |
| 
 | |
| class FileStatus:
 | |
|     """Issue status for a particular file. Stores issue counts for each type of issue."""
 | |
| 
 | |
|     def __init__(self, path):
 | |
|         self.path = path
 | |
|         issueNames = [
 | |
|             "has-inconsistent-line-endings",
 | |
|             "has-tabs",
 | |
|             "has-bad-indenting",
 | |
|             "has-trailing-whitespace",
 | |
|             "has-bad-character",
 | |
|             "has-empty-line-at-end-of-file",
 | |
|             "has-no-eol-character-at-end-of-file",
 | |
|         ]
 | |
|         self.issueCounts = dict.fromkeys(issueNames, 0)
 | |
| 
 | |
|     def incrementIssueCount(self, issueName):
 | |
|         assert issueName in self.issueCounts # catch typos in issueName
 | |
|         self.issueCounts[issueName] += 1
 | |
| 
 | |
|     def hasIssue(self, issueName):
 | |
|         return self.issueCounts[issueName] > 0
 | |
| 
 | |
|     def hasIssues(self):
 | |
|         return any(count > 0 for count in self.issueCounts.values())
 | |
| 
 | |
|     def issueSummaryString(self):
 | |
|         return str.join(", ", [name for name in self.issueCounts if self.issueCounts[name] > 0])
 | |
| 
 | |
| 
 | |
| def multilineCommentIsOpenAtEol(lineText, wasOpenAtStartOfLine):
 | |
|     isOpen = wasOpenAtStartOfLine
 | |
|     index = 0
 | |
|     end = len(lineText)
 | |
|     while index != -1 and index < end:
 | |
|         if isOpen:
 | |
|             index = lineText.find(b"*/", index)
 | |
|             if index != -1:
 | |
|                 isOpen = False
 | |
|                 index += 2
 | |
|         else:
 | |
|             index = lineText.find(b"/*", index)
 | |
|             if index != -1:
 | |
|                 isOpen = True
 | |
|                 index += 2
 | |
|     return isOpen
 | |
| 
 | |
| 
 | |
| def allowStrangeIndentOnFollowingLine(lineText):
 | |
|     """Compute whether a non-standard indent is allowed on the following line.
 | |
|     A line allows an unusual indent to follow if it is the beginning of a
 | |
|     multi-line function parameter list, an element of a function parameter list,
 | |
|     or an incomplete expression (binary operator, etc.).
 | |
|     """
 | |
|     s = lineText.strip(b" ")
 | |
|     if len(s) == 0:
 | |
|         return False
 | |
|     if s.rfind(b"*/") == (len(s) - 2):  # line has a trailing comment, strip it
 | |
|         commentStart = s.rfind(b"/*")
 | |
|         if commentStart != -1:
 | |
|             s = s[:commentStart].strip(b" ")
 | |
|             if len(s) == 0:
 | |
|                 return False
 | |
| 
 | |
|         if len(s) == 0:
 | |
|             return False
 | |
| 
 | |
|     okChars = b'(,\\+-/*=&|?:"'
 | |
|     if s[-1] in okChars: # non-comment program text has trailing okChar: '(' or ',' etc.
 | |
|         return True
 | |
|     return False
 | |
| 
 | |
| 
 | |
| def allowStrangeIndentOfLine(lineText):
 | |
|     """Compute whether a non-standard indent is allowed on the line.
 | |
|     A line is allowed an unusual indent if it is the continuation of an
 | |
|     incomplete expression (binary operator, etc.).
 | |
|     """
 | |
|     s = lineText.strip(b" ")
 | |
|     if len(s) == 0:
 | |
|         return False
 | |
| 
 | |
|     okChars = b'+-/*=&|?:)"'
 | |
|     if s[0] in okChars:
 | |
|         return True
 | |
|     return False
 | |
| 
 | |
| 
 | |
| # Run the checks over all files specified by [sourceFileTypes, dirs, excludePathParts]:
 | |
| 
 | |
| statusSummary = []
 | |
| for dir in dirs:
 | |
|     for ext in sourceFileTypes:
 | |
|         for path in Path(dir).rglob(ext):
 | |
|             if any(part in path.parts for part in excludePathParts):
 | |
|                 continue
 | |
| 
 | |
|             # during development, uncomment the following 2 lines and select a specific path:
 | |
|             #if not "qa" in path.parts:
 | |
|             #    continue
 | |
| 
 | |
|             data = path.read_bytes()
 | |
| 
 | |
|             status = FileStatus(path)
 | |
|             statusSummary.append(status)
 | |
| 
 | |
|             # Perform checks:
 | |
| 
 | |
|             # 1. Consistent line endings
 | |
|             # check and then normalize to \n line endings for the benefit of the rest of the program
 | |
|             if b"\r" in data and b"\n" in data:
 | |
|                 # CRLF (Windows) case: check for stray CR or LF, then convert CRLF to LF
 | |
|                 assert not b"\f" in data  # we'll use \f as a sentinel during conversion
 | |
|                 d = data.replace(b"\r\n", b"\f")
 | |
|                 if b"\r" in d:
 | |
|                     status.incrementIssueCount("has-inconsistent-line-endings")
 | |
|                     if verbose:
 | |
|                         print("error: {0} stray carriage return".format(path))
 | |
|                 if b"\n" in d:
 | |
|                     status.incrementIssueCount("has-inconsistent-line-endings")
 | |
|                     if verbose:
 | |
|                         print("error: {0} stray newline".format(path))
 | |
|                 data = d.replace(b"\f", b"\n")  # normalize line endings
 | |
|             elif b"\r" in data:
 | |
|                 # CR (Classic Mac) case: convert CR to LF
 | |
|                 data = d.replace(b"\r", b"\n")  # normalize line endings
 | |
|             else:
 | |
|                 # LF (Unix) case: no change
 | |
|                 pass
 | |
| 
 | |
|             lines = data.split(b"\n")  # relies on newline normalization above
 | |
| 
 | |
|             # 2. Absence of tabs
 | |
|             lineNo = 1
 | |
|             for line in lines:
 | |
|                 if b"\t" in line:
 | |
|                     status.incrementIssueCount("has-tabs")
 | |
|                     if verbose:
 | |
|                         print("error: {0}({1}) contains tab".format(path, lineNo))
 | |
|                 lineNo += 1
 | |
| 
 | |
|             data = data.replace(b"\t", b" "*indentSpaceCount) # normalize tabs to <indentSpaceCount> spaces for indent algorithm below
 | |
|             lines = data.split(b"\n") # recompute lines, relies on newline normalization above
 | |
| 
 | |
|             # 3. Correct leading whitespace / bad indenting
 | |
|             if checkBadIndenting:
 | |
|                 leadingWhitespaceRe = re.compile(b"^\s*")
 | |
|                 commentIsOpen = False
 | |
|                 previousLine = b""
 | |
|                 previousIndent = 0
 | |
|                 lineNo = 1
 | |
|                 for line in lines:
 | |
|                     if commentIsOpen:
 | |
|                         # don't check leading whitespace inside comments
 | |
|                         commentIsOpen = multilineCommentIsOpenAtEol(line, commentIsOpen)
 | |
|                         previousIndent = 0
 | |
|                     else:
 | |
|                         m = leadingWhitespaceRe.search(line)
 | |
|                         indent = m.end() - m.start()
 | |
|                         if indent != len(line): # ignore whitespace lines, they are considered trailing whitespace
 | |
|                             if indent % indentSpaceCount != 0 and indent != previousIndent:
 | |
|                                 # potential bad indents are not multiples of <indentSpaceCount>,
 | |
|                                 # and are not indented the same as the previous line
 | |
|                                 s = previousLine
 | |
|                                 if not allowStrangeIndentOnFollowingLine(previousLine) and not allowStrangeIndentOfLine(line):
 | |
|                                     status.incrementIssueCount("has-bad-indenting")
 | |
|                                     if verbose or verboseBadIndenting:
 | |
|                                         print("error: {0}({1}) bad indent: {2}".format(path, lineNo, indent))
 | |
|                                         print(line)
 | |
|                         commentIsOpen = multilineCommentIsOpenAtEol(line, commentIsOpen)
 | |
|                         previousIndent = indent
 | |
|                     previousLine = line
 | |
|                     lineNo += 1
 | |
| 
 | |
|             # 4. No trailing whitespace
 | |
|             trailingWhitespaceRe = re.compile(b"\s*$")
 | |
|             lineNo = 1
 | |
|             for line in lines:
 | |
|                 m = trailingWhitespaceRe.search(line)
 | |
|                 trailing = m.end() - m.start()
 | |
|                 if trailing > 0:
 | |
|                     status.incrementIssueCount("has-trailing-whitespace")
 | |
|                     if verbose:
 | |
|                         print("error: {0}({1}) trailing whitespace:".format(path, lineNo))
 | |
|                         print(line)
 | |
|                 lineNo += 1
 | |
| 
 | |
|             # 5. No non-ASCII or weird control characters
 | |
|             badCharactersRe = re.compile(b"[^\t\r\n\x20-\x7E]+")
 | |
|             lineNo = 1
 | |
|             for line in lines:
 | |
|                 m = badCharactersRe.search(line)
 | |
|                 if m:
 | |
|                     bad = m.end() - m.start()
 | |
|                     if bad > 0:
 | |
|                         status.incrementIssueCount("has-bad-character")
 | |
|                         if verbose:
 | |
|                             print("error: {0}({1}) bad character:".format(path, lineNo))
 | |
|                             print(line)
 | |
|                 lineNo += 1
 | |
| 
 | |
|             # 6. Require EOL at EOF
 | |
|             if len(data) == 0:
 | |
|                 status.incrementIssueCount("has-no-eol-character-at-end-of-file")
 | |
|                 if verbose:
 | |
|                     lineNo = 1
 | |
|                     print("error: {0}({1}) no end-of-line at end-of-file (empty file)".format(path, lineNo))
 | |
|             else:
 | |
|                 lastChar = data[-1]
 | |
|                 if lastChar != b"\n"[0]:
 | |
|                     status.incrementIssueCount("has-no-eol-character-at-end-of-file")
 | |
|                     if verbose:
 | |
|                         lineNo = len(lines)
 | |
|                         print("error: {0}({1}) no end-of-line at end-of-file".format(path, lineNo))
 | |
| 
 | |
|             # 7. No "empty" (or whitespace) lines at end-of-file.
 | |
|             # Cases:
 | |
|             #   1. There is an EOL at EOF. Since the lines array is constructed by splitting on '\n',
 | |
|             #      the final element in the lines array will be an empty string. This is expeced and allowed.
 | |
|             #      Then continue to check for earlier empty lines.
 | |
|             #   2. There is no EOF at EOL.
 | |
|             #      Check for empty lines, including the final line.
 | |
|             expectEmptyFinalLine = not status.hasIssue("has-no-eol-character-at-end-of-file") # i.e. we have EOL at EOF
 | |
|             finalLineNo = len(lines)
 | |
|             lineNo = finalLineNo
 | |
|             for line in reversed(lines):
 | |
|                 if lineNo == finalLineNo and expectEmptyFinalLine:
 | |
|                     assert len(line) == 0 # this is guaranteed, since lines = data.split('\n') and there is an EOL at EOF
 | |
|                 else:
 | |
|                     s = line.strip(b" ") # whitespace-only-lines count as empty
 | |
|                     if len(s) == 0:
 | |
|                         status.incrementIssueCount("has-empty-line-at-end-of-file")
 | |
|                         if verbose:
 | |
|                             print("error: {0}({1}) empty line at end-of-file".format(path, lineNo))
 | |
|                     else:
 | |
|                         break # stop checking once we encounter a non-empty line
 | |
|                 lineNo -= 1
 | |
| 
 | |
| 
 | |
| print("SUMMARY")
 | |
| print("=======")
 | |
| issuesFound = False
 | |
| for s in statusSummary:
 | |
|     if s.hasIssues():
 | |
|         issuesFound = True
 | |
|         print("error: " + str(s.path) + " (" + s.issueSummaryString() + ")")
 | |
| 
 | |
| if issuesFound:
 | |
|     sys.exit(1)
 | |
| else:
 | |
|     print("all good.")
 | |
|     sys.exit(0)
 |