blob: d9a98ee85c5e18c52dc9976dfce5ccc6c6a1a4ca [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"path/filepath"
"regexp"
"sort"
"strings"
tricium "infra/tricium/api/v1"
)
const (
category = "ObjectiveCStyle"
)
var (
fileAllowlist = []string{".m", ".mm"}
// Matches methods with get prefix. Captures return type and selector.
methodImplementationRegexp = regexp.MustCompile(`[-+]\s\((.+)\)(get.*)\s\{`)
// Matches delegate properties. Captures property specifiers.
delegatePropertyRegexp = regexp.MustCompile(`@property\((.*)\)\s*id<.*>\s.*[dD]elegate;`)
// Matches all pointer properties. Captures property specifiers.
pointerPropertyRegexp = regexp.MustCompile(`@property(.*)(\*|id<.*>).*;`)
)
func main() {
inputDir := flag.String("input", "", "Path to root of Tricium input")
outputDir := flag.String("output", "", "Path to root of Tricium output")
flag.Parse()
if flag.NArg() != 0 {
log.Panicf("Unexpected argument.")
}
// Read Tricium input FILES data.
input := &tricium.Data_Files{}
if err := tricium.ReadDataType(*inputDir, input); err != nil {
log.Panicf("Failed to read FILES data: %v", err)
}
log.Printf("Read FILES data.")
// Create RESULTS data.
output := &tricium.Data_Results{}
for _, file := range input.Files {
if file.IsBinary {
log.Printf("Skipping binary file %q.", file.Path)
continue
}
if !isAllowed(file.Path) {
log.Printf("Skipping file: %q.", file.Path)
continue
}
if comments := checkSourceFile(*inputDir, file.Path); comments != nil {
for _, comment := range comments {
log.Printf("%s: %s", file.Path, comment.Category)
output.Comments = append(output.Comments, comment)
}
}
}
// Write Tricium RESULTS data.
path, err := tricium.WriteDataType(*outputDir, output)
if err != nil {
log.Panicf("Failed to write RESULTS data: %v", err)
}
log.Printf("Wrote RESULTS data to %q.", path)
}
func isAllowed(path string) bool {
for _, ext := range fileAllowlist {
if ext == filepath.Ext(path) || ext == filepath.Base(path) {
return true
}
}
return false
}
// checkSourceFile performs various regex based checks and return comments for
// found violations. Current checks include:
// - flagging methods where "get" prefix was misused
// - flagging strong delegates
//
// dirPath is the path to the repo directory base path, and filePath
// is the relative path from the base to the file.
func checkSourceFile(base string, path string) []*tricium.Data_Comment {
content, err := ioutil.ReadFile(filepath.Join(base, path))
if err != nil {
log.Panicf("Failed to read file: %v", err)
}
contentString := string(content)
matches := methodImplementationRegexp.FindAllStringSubmatchIndex(contentString, -1)
// Look for methods where "get" prefix was misused.
var comments []*tricium.Data_Comment = nil
converter := newCharIndexToLineConverter(contentString)
for _, match := range matches {
functionStartIndex := match[0]
functionEndIndex := match[1]
returnType := contentString[match[2]:match[3]]
selector := contentString[match[4]:match[5]]
if returnType != "void" || !strings.Contains(selector, ":") {
functionStartLine := converter.getStartLine(functionStartIndex)
comment := foundGetPrefix(path,
functionStartLine,
converter.getEndLine(functionEndIndex),
converter.getEndChar(functionStartLine))
comments = append(comments, comment)
}
}
// Look for strong delegate properties. In Objective-C the normal convention
// is to have "weak" delegate properties. "strong" delegate properties can
// cause retain cycles, which is quite error prone, because "strong" is
// default.
matches = delegatePropertyRegexp.FindAllStringSubmatchIndex(contentString, -1)
for _, match := range matches {
propertyStartIndex := match[0]
propertyEndIndex := match[1]
specifiers := contentString[match[2]:match[3]]
if !strings.Contains(specifiers, "weak") {
propertyStartLine := converter.getStartLine(propertyStartIndex)
comment := foundStrongDelegate(path,
propertyStartLine,
converter.getEndLine(propertyEndIndex),
converter.getEndChar(propertyStartLine))
comments = append(comments, comment)
}
}
// Look for properties that do not have any ownership specifier. By default,
// properties have "strong" ownership, which can lead to dependency cycles.
matches = pointerPropertyRegexp.FindAllStringSubmatchIndex(contentString, -1)
for _, match := range matches {
propertyStartIndex := match[0]
propertyEndIndex := match[1]
specifiers := contentString[match[2]:match[3]]
if !strings.Contains(specifiers, "weak") &&
!strings.Contains(specifiers, "strong") &&
!strings.Contains(specifiers, "readonly") &&
!strings.Contains(specifiers, "copy") &&
!strings.Contains(specifiers, "assign") {
propertyStartLine := converter.getStartLine(propertyStartIndex)
comment := foundPropertyWithNoOwnershipSpecifier(path,
propertyStartLine,
converter.getEndLine(propertyEndIndex),
converter.getEndChar(propertyStartLine))
comments = append(comments, comment)
}
}
return comments
}
func foundGetPrefix(path string, startLine int, endLine int, functionEndChar int) *tricium.Data_Comment {
return &tricium.Data_Comment{
Category: fmt.Sprintf("ObjectiveCStyle/Get"),
Message: "The use of \"get\" is unnecessary, unless one or more values " +
"are returned indirectly. See: " +
"https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CodingGuidelines/Articles/NamingMethods.html#:~:text=The%20use%20of%20%22get%22%20is%20unnecessary,%20unless%20one%20or%20more%20values%20are%20returned%20indirectly.",
Path: path,
StartLine: int32(startLine),
EndLine: int32(endLine),
StartChar: 0, // always zero
EndChar: int32(functionEndChar),
}
}
func foundStrongDelegate(path string, startLine int, endLine int, functionEndChar int) *tricium.Data_Comment {
return &tricium.Data_Comment{
Category: fmt.Sprintf("ObjectiveCStyle/StrongDelegate"),
Message: "In Objective-C delegates are normally weak. Strong delegates can cause retain cycles.",
Path: path,
StartLine: int32(startLine),
EndLine: int32(endLine),
StartChar: 0, // always zero
EndChar: int32(functionEndChar),
}
}
func foundPropertyWithNoOwnershipSpecifier(path string, startLine, endLine, functionEndChar int) *tricium.Data_Comment {
return &tricium.Data_Comment{
Category: "ObjectiveCStyle/ExplicitOwnership",
Message: "Consider using an explicit ownership specifier. The default is strong, which can cause retain cycles.",
Path: path,
StartLine: int32(startLine),
EndLine: int32(endLine),
StartChar: 0, // always zero
EndChar: int32(functionEndChar),
}
}
// CharIndexToLineConverter converts character index obtained from regex to line
// number required by tricium. Line numbers for Tricium comments are 1-based
// because that's how it is in the Gerrit API, where line 0 represents a
// file-level comment, and line numbers starting with 1 are displayed next to
// actual lines.
type CharIndexToLineConverter interface {
getStartLine(startIndex int) int
getEndLine(endIndex int) int
getEndChar(startLine int) int
}
func newCharIndexToLineConverter(content string) CharIndexToLineConverter {
return charIndexToLineConverter{content, nil, nil}
}
func (converter charIndexToLineConverter) getStartLine(startIndex int) int {
return sort.SearchInts(getLookupTable(converter), startIndex) + 2
}
func (converter charIndexToLineConverter) getEndLine(endIndex int) int {
return sort.SearchInts(getLookupTable(converter), endIndex) + 1
}
func (converter charIndexToLineConverter) getEndChar(startLine int) int {
return len(getLines(converter)[startLine-1])
}
type charIndexToLineConverter struct {
// Full content of the source file.
content string
// |content| string split by "\n". Lazily created.
lines []string
// Dynamic programming lookup table of the same length as |lines|. Each value
// in the table represents number of characters in current and all previous
// lines. Lazily created.
lookupTable []int
}
func getLines(converter charIndexToLineConverter) []string {
if converter.lines == nil {
converter.lines = strings.Split(converter.content, "\n")
}
return converter.lines
}
func getLookupTable(converter charIndexToLineConverter) []int {
if converter.lookupTable == nil {
previousLinesLen := 0
for _, line := range getLines(converter) {
currentLineLen := len(line) + len("\n")
converter.lookupTable = append(converter.lookupTable, previousLinesLen+currentLineLen)
previousLinesLen += currentLineLen
}
}
return converter.lookupTable
}