Skip to content

Create tool for linting Arduino projects #1

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 54 commits into from
Oct 30, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
1646215
Add first draft of library.properties schema
per1234 Sep 16, 2020
e17c4d7
Initial commit of code
per1234 Sep 17, 2020
ecdce06
Use consistent approach to handling custom types
per1234 Sep 20, 2020
12411ac
Rename projects package to project
per1234 Sep 20, 2020
b261e7b
Schema: update name pattern to new specification
per1234 Sep 21, 2020
4d024ed
Schema: remove the non-capturing group syntax from the version regex
per1234 Sep 21, 2020
89d6372
Schema: fix the misspelled optional field name regex
per1234 Sep 21, 2020
c04946b
Correctly handle "NotRun" check results
per1234 Sep 21, 2020
c39f587
Create libraryproperties package and add validation helper functions
per1234 Sep 21, 2020
bd5b370
Add "notice" checklevel
per1234 Sep 21, 2020
416233a
Rename "Skipped" checkresult to "Skip"
per1234 Sep 21, 2020
fce1f2c
Add reminder comment re: handling exit status
per1234 Sep 21, 2020
8b527e4
Add more checks
per1234 Sep 21, 2020
132ce6d
Schema: make name regex more minimal
per1234 Sep 21, 2020
abd360f
Split check functions into separate files according to project type
per1234 Sep 21, 2020
3682cee
Add check for .pde sketch file extension
per1234 Sep 21, 2020
ac1bec4
Output error messages from projects.FindProjects()
per1234 Sep 21, 2020
27a6787
Fix check configuration system
per1234 Sep 21, 2020
f88be58
Start setting up logging
per1234 Sep 21, 2020
19ee67b
Fix bug with project discovery system
per1234 Sep 21, 2020
26046fb
Provide meaningful output when checks don't run due to required data
per1234 Sep 21, 2020
ed14838
Add a TODO comment
per1234 Sep 21, 2020
2c5c11a
Set up configuration to be able to easily experiment with different s…
per1234 Sep 21, 2020
f8da280
Remove "Pass" check level
per1234 Sep 21, 2020
cd49ac3
Add/improve code comments
per1234 Sep 21, 2020
952112d
Add TODO comment re: coniguration.init() vs configuration.Initialize()
per1234 Sep 21, 2020
00178a7
Rename checkconfigurations.Type.Name field to checkconfigurations.Typ…
per1234 Sep 21, 2020
c1c3b9c
Rename configuration.SuperprojectType() to configuration.Superproject…
per1234 Sep 21, 2020
63a68f5
Rename project.findProjects() to project.findProjectsUnderPath()
per1234 Sep 21, 2020
8547011
Add a getter for checkconfigurations.configurations
per1234 Sep 21, 2020
f9027f8
Improve organization of code
per1234 Sep 21, 2020
42039d9
Return error when check run configuration is missing
per1234 Sep 21, 2020
658cdf6
Use packageindex.HasValidExtension() in package index detection code
per1234 Sep 22, 2020
b1a0224
Define supported and valid library examples folder names in the libra…
per1234 Sep 22, 2020
598ae7f
Define valid platform bundled libraries folder names in the platform …
per1234 Sep 22, 2020
e5133ae
Add go.mod files
Sep 22, 2020
8af60f3
Only print skipped check information to log
per1234 Sep 22, 2020
a93a6d0
Add support for "json" output format
per1234 Sep 22, 2020
528aa98
Set exit status according to check results
per1234 Sep 22, 2020
2c29553
Add support for report file option
per1234 Sep 22, 2020
9bde283
Return boolean variable values directly from functions
per1234 Oct 29, 2020
b3457a2
Reduce verbosity of Boolean comparisons
per1234 Oct 29, 2020
b40200e
Remove unncessary use of String() method
per1234 Oct 29, 2020
a842f06
Eliminate duplicate code in subproject discovery
per1234 Oct 29, 2020
2fe0250
Panic if subproject discovery was not configured for project type
per1234 Oct 29, 2020
918d845
Refactor .pde sketch check function's output code
per1234 Oct 29, 2020
2517a0b
Add doc comments for check functions
per1234 Oct 29, 2020
9ddc1bb
Add project type matcher method
per1234 Oct 29, 2020
e692238
Use more apropriate project type in log message
per1234 Oct 29, 2020
561d88e
Use more appropriate variable name for project type filter
per1234 Oct 29, 2020
b9f951b
Refactor project identification functions
per1234 Oct 29, 2020
fbe79ee
Support multiple report instances
per1234 Oct 29, 2020
6894049
Refactor names in the result package
per1234 Oct 29, 2020
09bbd6a
Use an "enum" for the output format types
per1234 Oct 29, 2020
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
Prev Previous commit
Next Next commit
Add support for "json" output format
  • Loading branch information
per1234 committed Oct 30, 2020
commit a93a6d06496ca5198a103676ab8f216c7688d1b7
45 changes: 17 additions & 28 deletions check/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,15 @@
package check

import (
"bytes"
"fmt"
"os"
"text/template"

"github.com/arduino/arduino-check/check/checkconfigurations"
"github.com/arduino/arduino-check/check/checkdata"
"github.com/arduino/arduino-check/check/checklevel"
"github.com/arduino/arduino-check/check/checkresult"
"github.com/arduino/arduino-check/configuration"
"github.com/arduino/arduino-check/configuration/checkmode"
"github.com/arduino/arduino-check/project"
"github.com/arduino/arduino-check/result"
"github.com/arduino/arduino-check/result/feedback"
"github.com/sirupsen/logrus"
)
Expand All @@ -36,20 +33,23 @@ func RunChecks(project project.Type) {
continue
}

fmt.Printf("Running check %s: ", checkConfiguration.ID)
result, output := checkConfiguration.CheckFunction()
fmt.Printf("%s\n", result.String())
if result == checkresult.NotRun {
// TODO: make the check functions output an explanation for why they didn't run
fmt.Printf("%s: %s\n", checklevel.Notice, output)
} else if result != checkresult.Pass {
checkLevel, err := checklevel.CheckLevel(checkConfiguration)
if err != nil {
feedback.Errorf("Error while determining check level: %v", err)
os.Exit(1)
}
fmt.Printf("%s: %s\n", checkLevel.String(), message(checkConfiguration.MessageTemplate, output))
// Output will be printed after all checks are finished when configured for "json" output format
if configuration.OutputFormat() == "text" {
fmt.Printf("Running check %s: ", checkConfiguration.ID)
}
checkResult, checkOutput := checkConfiguration.CheckFunction()
reportText := result.Record(project, checkConfiguration, checkResult, checkOutput)
if configuration.OutputFormat() == "text" {
fmt.Print(reportText)
}
}

// Checks are finished for this project, so summarize its check results in the report.
result.AddProjectSummaryReport(project)

if configuration.OutputFormat() == "text" {
// Print the project check results summary.
fmt.Print(result.ProjectSummaryText(project))
}
}

Expand Down Expand Up @@ -88,14 +88,3 @@ func shouldRun(checkConfiguration checkconfigurations.Type, currentProject proje

return false, fmt.Errorf("Check %s is incorrectly configured", checkConfiguration.ID)
}

// message fills the message template provided by the check configuration with the check output.
// TODO: make checkOutput a struct to allow for more advanced message templating
func message(templateText string, checkOutput string) string {
messageTemplate := template.Must(template.New("messageTemplate").Parse(templateText))

messageBuffer := new(bytes.Buffer)
messageTemplate.Execute(messageBuffer, checkOutput)

return messageBuffer.String()
}
12 changes: 12 additions & 0 deletions configuration/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@ func Initialize() {
// TODO configuration according to command line input
// TODO validate target path value, exit if not found
// TODO support multiple paths
// TODO validate output format input

targetPath = paths.New("e:/electronics/arduino/libraries/arduino-check-test-library")

// customCheckModes[checkmode.Permissive] = false
// customCheckModes[checkmode.LibraryManagerSubmission] = false
// customCheckModes[checkmode.LibraryManagerIndexed] = false
// customCheckModes[checkmode.Official] = false
// superprojectType = projecttype.All

outputFormat = "json"

logrus.SetLevel(logrus.PanicLevel)

logrus.WithFields(logrus.Fields{
Expand Down Expand Up @@ -52,6 +57,13 @@ func Recursive() bool {
return recursive
}

var outputFormat string

// OutputFormat returns the tool output format configuration value.
func OutputFormat() string {
return outputFormat
}

var targetPath *paths.Path

// TargetPath returns the projects search path.
Expand Down
1 change: 1 addition & 0 deletions configuration/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
func setDefaults() {
superprojectTypeFilter = projecttype.All
recursive = true
outputFormat = "text"
// TODO: targetPath defaults to current path
}

Expand Down
20 changes: 20 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,43 @@
package main

import (
"fmt"
"os"

"github.com/arduino/arduino-check/check"
"github.com/arduino/arduino-check/configuration"
"github.com/arduino/arduino-check/project"
"github.com/arduino/arduino-check/result"
"github.com/arduino/arduino-check/result/feedback"
)

func main() {
configuration.Initialize()
// Must be called after configuration.Initialize()
result.Initialize()

projects, err := project.FindProjects()
if err != nil {
feedback.Errorf("Error while finding projects: %v", err)
os.Exit(1)
}

for _, project := range projects {
check.RunChecks(project)
}

// All projects have been checked, so summarize their check results in the report.
result.AddSummaryReport()

if configuration.OutputFormat() == "text" {
if len(projects) > 1 {
// There are multiple projects, print the summary of check results for all projects.
fmt.Print(result.SummaryText())
}
} else {
// Print the complete JSON formatted report.
fmt.Println(result.JSONReport())
}

// TODO: set exit status according to check results
}
231 changes: 231 additions & 0 deletions result/result.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// Package result records check results and provides reports and summary text on those results.
package result

import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"os"

"github.com/arduino/arduino-check/check/checkconfigurations"
"github.com/arduino/arduino-check/check/checklevel"
"github.com/arduino/arduino-check/check/checkresult"
"github.com/arduino/arduino-check/configuration"
"github.com/arduino/arduino-check/configuration/checkmode"
"github.com/arduino/arduino-check/project"
"github.com/arduino/arduino-check/result/feedback"
"github.com/arduino/go-paths-helper"
)

type reportType struct {
Configuration toolConfigurationReportType `json:"configuration"`
Projects []projectReportType `json:"projects"`
Summary summaryReportType `json:"summary"`
}

type toolConfigurationReportType struct {
Paths []*paths.Path `json:"paths"`
ProjectType string `json:"projectType"`
Recursive bool `json:"recursive"`
}

type projectReportType struct {
Path *paths.Path `json:"path"`
ProjectType string `json:"projectType"`
Configuration projectConfigurationReportType `json:"configuration"`
Checks []checkReportType `json:"checks"`
Summary summaryReportType `json:"summary"`
}

type projectConfigurationReportType struct {
Permissive bool `json:"permissive"`
LibraryManagerSubmit bool `json:"libraryManagerSubmit"`
LibraryManagerUpdate bool `json:"libraryManagerUpdate"`
Official bool `json:"official"`
}

type checkReportType struct {
Category string `json:"category"`
Subcategory string `json:"subcategory"`
ID string `json:"ID"`
Brief string `json:"brief"`
Description string `json:"description"`
Result string `json:"result"`
Level string `json:"level"`
Message string `json:"message"`
}

type summaryReportType struct {
Pass bool `json:"pass"`
WarningCount int `json:"warningCount"`
ErrorCount int `json:"errorCount"`
}

var report reportType

// Initialize adds the tool configuration data to the report.
func Initialize() {
report.Configuration = toolConfigurationReportType{
Paths: []*paths.Path{configuration.TargetPath()},
ProjectType: configuration.SuperprojectTypeFilter().String(),
Recursive: configuration.Recursive(),
}
}

// Record records the result of a check and returns a text summary for it.
func Record(checkedProject project.Type, checkConfiguration checkconfigurations.Type, checkResult checkresult.Type, checkOutput string) string {
checkMessage := message(checkConfiguration.MessageTemplate, checkOutput)

checkLevel, err := checklevel.CheckLevel(checkConfiguration)
if err != nil {
feedback.Errorf("Error while determining check level: %v", err)
os.Exit(1)
}

summaryText := fmt.Sprintf("%v\n", checkResult.String())

if checkResult == checkresult.NotRun {
// TODO: make the check functions output an explanation for why they didn't run
summaryText += fmt.Sprintf("%s: %s\n", checklevel.Notice.String(), checkOutput)
} else if checkResult != checkresult.Pass {
summaryText += fmt.Sprintf("%s: %s\n", checkLevel.String(), checkMessage)
}

checkReport := checkReportType{
Category: checkConfiguration.Category,
Subcategory: checkConfiguration.Subcategory,
ID: checkConfiguration.ID,
Brief: checkConfiguration.Brief,
Description: checkConfiguration.Description,
Result: checkResult.String(),
Level: checkLevel.String(),
Message: checkMessage,
}

reportExists, projectReportIndex := getProjectReportIndex(checkedProject.Path)
if !reportExists {
// There is no existing report for this project.
report.Projects = append(
report.Projects,
projectReportType{
Path: checkedProject.Path,
ProjectType: checkedProject.ProjectType.String(),
Configuration: projectConfigurationReportType{
Permissive: configuration.CheckModes(checkedProject.ProjectType)[checkmode.Permissive],
LibraryManagerSubmit: configuration.CheckModes(checkedProject.ProjectType)[checkmode.Permissive],
LibraryManagerUpdate: configuration.CheckModes(checkedProject.ProjectType)[checkmode.LibraryManagerIndexed],
Official: configuration.CheckModes(checkedProject.ProjectType)[checkmode.Official],
},
Checks: []checkReportType{checkReport},
},
)
} else {
// There's already a report for this project, just add the checks report to it
report.Projects[projectReportIndex].Checks = append(report.Projects[projectReportIndex].Checks, checkReport)
}

return summaryText
}

// AddProjectSummaryReport summarizes the results of all checks on the given project and adds it to the report.
func AddProjectSummaryReport(checkedProject project.Type) {
reportExists, projectReportIndex := getProjectReportIndex(checkedProject.Path)
if !reportExists {
panic(fmt.Sprintf("Unable to find report for %v when generating report summary", checkedProject.Path))
}

pass := true
warningCount := 0
errorCount := 0
for _, checkReport := range report.Projects[projectReportIndex].Checks {
if checkReport.Result == checkresult.Fail.String() {
if checkReport.Level == checklevel.Warning.String() {
warningCount += 1
} else if checkReport.Level == checklevel.Error.String() {
errorCount += 1
pass = false
}
}
}

report.Projects[projectReportIndex].Summary = summaryReportType{
Pass: pass,
WarningCount: warningCount,
ErrorCount: errorCount,
}
}

// ProjectSummaryText returns a text summary of the check results for the given project.
func ProjectSummaryText(checkedProject project.Type) string {
reportExists, projectReportIndex := getProjectReportIndex(checkedProject.Path)
if !reportExists {
panic(fmt.Sprintf("Unable to find report for %v when generating report summary text", checkedProject.Path))
}

projectSummaryReport := report.Projects[projectReportIndex].Summary
return fmt.Sprintf("\nFinished checking project. Results:\nWarning count: %v\nError count: %v\nChecks passed: %v\n\n", projectSummaryReport.WarningCount, projectSummaryReport.ErrorCount, projectSummaryReport.Pass)
}

// AddSummaryReport summarizes the check results for all projects and adds it to the report.
func AddSummaryReport() {
pass := true
warningCount := 0
errorCount := 0
for _, projectReport := range report.Projects {
if !projectReport.Summary.Pass {
pass = false
}
warningCount += projectReport.Summary.WarningCount
errorCount += projectReport.Summary.ErrorCount
}

report.Summary = summaryReportType{
Pass: pass,
WarningCount: warningCount,
ErrorCount: errorCount,
}
}

// SummaryText returns a text summary of the cumulative check results.
func SummaryText() string {
return fmt.Sprintf("Finished checking projects. Results:\nWarning count: %v\nError count: %v\nChecks passed: %v\n", report.Summary.WarningCount, report.Summary.ErrorCount, report.Summary.Pass)
}

// Report returns a JSON formatted report of checks on all projects.
func JSONReport() string {
return string(jsonReportRaw())
}

func jsonReportRaw() []byte {
reportJSON, err := json.MarshalIndent(report, "", " ")
if err != nil {
panic(fmt.Sprintf("Error while formatting checks report: %v", err))
}

return reportJSON
}

func getProjectReportIndex(projectPath *paths.Path) (bool, int) {
var index int
var projectReport projectReportType
for index, projectReport = range report.Projects {
if projectReport.Path == projectPath {
return true, index
}
}

// There is no element in the report for this project.
return false, index + 1
}

// message fills the message template provided by the check configuration with the check output.
// TODO: make checkOutput a struct to allow for more advanced message templating
func message(templateText string, checkOutput string) string {
messageTemplate := template.Must(template.New("messageTemplate").Parse(templateText))

messageBuffer := new(bytes.Buffer)
messageTemplate.Execute(messageBuffer, checkOutput)

return messageBuffer.String()
}