Skip to content

Add support for version constraints in depends field #182

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 3 commits into from
Jun 3, 2021
Merged
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
16 changes: 3 additions & 13 deletions etc/schemas/arduino-library-properties-definitions-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,7 @@
"allowedCharacters": {
"allOf": [
{
"$ref": "#/definitions/propertiesObjects/depends/base/definitions/patternObject"
},
{
"not": {
"$comment": "The depends property is a comma separated list of names, so a valid name pattern is a valid depends pattern with the comma excluded",
"pattern": "^.*,.*$"
}
"pattern": "^(([a-zA-Z][a-zA-Z0-9 _.\\-]*)|([0-9][a-zA-Z0-9 _.\\-]*[a-zA-Z][a-zA-Z0-9 _.\\-]*))*$"
}
]
}
Expand Down Expand Up @@ -494,18 +488,14 @@
},
"depends": {
"base": {
"definitions": {
"patternObject": {
"pattern": "^(([a-zA-Z][a-zA-Z0-9 _\\.\\-,]*)|([0-9][a-zA-Z0-9 _\\.\\-]*[a-zA-Z][a-zA-Z0-9 _\\.\\-,]*))*$"
}
},
"object": {
"allOf": [
{
"type": "string"
},
{
"$ref": "#/definitions/propertiesObjects/depends/base/definitions/patternObject"
"$comment": "Based on #/definitions/propertiesObjects/name/base/definitions/patternObjects/allowedCharacters and general-definitions-schema.json#/definitions/patternObjects/relaxedSemver",
"pattern": "^((((([a-zA-Z][a-zA-Z0-9 _.\\-]*)|([0-9][a-zA-Z0-9 _.\\-]*[a-zA-Z][a-zA-Z0-9 _.\\-]*))+( \\( *(<|<=|=|>=|>)(0|[1-9]\\d*)(\\.(0|[1-9]\\d*))?(\\.(0|[1-9]\\d*))?(-((0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(\\.(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\\+([0-9a-zA-Z-]+(\\.[0-9a-zA-Z-]+)*))? *\\) *)?), *)*((([a-zA-Z][a-zA-Z0-9 _.\\-]*)|([0-9][a-zA-Z0-9 _.\\-]*[a-zA-Z][a-zA-Z0-9 _.\\-]*))+( \\( *(<|<=|=|>=|>)(0|[1-9]\\d*)(\\.(0|[1-9]\\d*))?(\\.(0|[1-9]\\d*))?(-((0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(\\.(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\\+([0-9a-zA-Z-]+(\\.[0-9a-zA-Z-]+)*))? *\\) *)?))?$"
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,13 +330,60 @@ func TestPropertiesUrlFormat(t *testing.T) {

func TestPropertiesDependsPattern(t *testing.T) {
testTables := []propertyValueTestTable{
{"Valid name", "foo", compliancelevel.Permissive, assert.False},
{"Valid name", "foo", compliancelevel.Specification, assert.False},
{"Valid name", "foo", compliancelevel.Strict, assert.False},

{"Valid names", "foo,bar", compliancelevel.Permissive, assert.False},
{"Valid names", "foo,bar", compliancelevel.Specification, assert.False},
{"Valid names", "foo,bar", compliancelevel.Strict, assert.False},

{"Trailing comma", "foo,", compliancelevel.Permissive, assert.True},
{"Trailing comma", "foo,", compliancelevel.Specification, assert.True},
{"Trailing comma", "foo,", compliancelevel.Strict, assert.True},

{"Invalid characters", "-foo", compliancelevel.Permissive, assert.True},
{"Invalid characters", "-foo", compliancelevel.Specification, assert.True},
{"Invalid characters", "-foo", compliancelevel.Strict, assert.True},

{"Empty", "", compliancelevel.Permissive, assert.False},
{"Empty", "", compliancelevel.Specification, assert.False},
{"Empty", "", compliancelevel.Strict, assert.False},

{"<version", "foo (<1.2.3)", compliancelevel.Permissive, assert.False},
{"<version", "foo (<1.2.3)", compliancelevel.Specification, assert.False},
{"<version", "foo (<1.2.3)", compliancelevel.Strict, assert.False},
{"<=version", "foo (<=1.2.3)", compliancelevel.Permissive, assert.False},
{"<=version", "foo (<=1.2.3)", compliancelevel.Specification, assert.False},
{"<=version", "foo (<=1.2.3)", compliancelevel.Strict, assert.False},
{"=version", "foo (=1.2.3)", compliancelevel.Permissive, assert.False},
{"=version", "foo (=1.2.3)", compliancelevel.Specification, assert.False},
{"=version", "foo (=1.2.3)", compliancelevel.Strict, assert.False},
{">=version", "foo (>=1.2.3)", compliancelevel.Permissive, assert.False},
{">=version", "foo (>=1.2.3)", compliancelevel.Specification, assert.False},
{">=version", "foo (>=1.2.3)", compliancelevel.Strict, assert.False},
{">version", "foo (>1.2.3)", compliancelevel.Permissive, assert.False},
{">version", "foo (>1.2.3)", compliancelevel.Specification, assert.False},
{">version", "foo (>1.2.3)", compliancelevel.Strict, assert.False},

{"Relaxed version", "foo (=1.2)", compliancelevel.Permissive, assert.False},
{"Relaxed version", "foo (=1.2)", compliancelevel.Specification, assert.False},
{"Relaxed version", "foo (=1.2)", compliancelevel.Strict, assert.False},
{"Pre-release version", "foo (=1.2.3-rc1)", compliancelevel.Permissive, assert.False},
{"Pre-release version", "foo (=1.2.3-rc1)", compliancelevel.Specification, assert.False},
{"Pre-release version", "foo (=1.2.3-rc1)", compliancelevel.Strict, assert.False},

{"Invalid version", "foo (bar)", compliancelevel.Permissive, assert.True},
{"Invalid version", "foo (bar)", compliancelevel.Specification, assert.True},
{"Invalid version", "foo (bar)", compliancelevel.Strict, assert.True},

{"Version w/o space", "foo(>1.2.3)", compliancelevel.Permissive, assert.True},
{"Version w/o space", "foo(>1.2.3)", compliancelevel.Specification, assert.True},
{"Version w/o space", "foo(>1.2.3)", compliancelevel.Strict, assert.True},

{"Names w/ version", "foo (<=1.2.3),bar", compliancelevel.Permissive, assert.False},
{"Names w/ version", "foo (<=1.2.3),bar", compliancelevel.Specification, assert.False},
{"Names w/ version", "foo (<=1.2.3),bar", compliancelevel.Strict, assert.False},
}

checkPropertyPatternMismatch("depends", testTables, t)
Expand Down
34 changes: 23 additions & 11 deletions internal/project/projectdata/library.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,20 @@
package projectdata

import (
"encoding/json"
"io/ioutil"
"io"
"net/http"
"os"

"github.com/arduino/arduino-cli/arduino/libraries"
"github.com/arduino/arduino-cli/arduino/libraries/librariesmanager"
"github.com/arduino/arduino-lint/internal/configuration"
"github.com/arduino/arduino-lint/internal/configuration/rulemode"
"github.com/arduino/arduino-lint/internal/project"
"github.com/arduino/arduino-lint/internal/project/library/libraryproperties"
"github.com/arduino/arduino-lint/internal/result/feedback"
"github.com/arduino/arduino-lint/internal/rule/schema"
"github.com/arduino/arduino-lint/internal/rule/schema/compliancelevel"
"github.com/arduino/go-paths-helper"
"github.com/arduino/go-properties-orderedmap"
"github.com/client9/misspell"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -60,23 +61,34 @@ func InitializeForLibrary(project project.Type) {

// Download the Library Manager index if needed.
if !configuration.RuleModes(project.SuperprojectType)[rulemode.LibraryManagerIndexing] && libraryManagerIndex == nil {
url := "http://downloads.arduino.cc/libraries/library_index.json"
httpResponse, err := http.Get(url)
// Set up the temporary folder for the index
libraryIndexFolderPath, err := paths.TempDir().MkTempDir("arduino-lint-library-index-folder")
defer libraryIndexFolderPath.RemoveAll()
if err != nil {
feedback.Errorf("Unable to download Library Manager index from %s: %s", err, url)
panic(err)
}
libraryIndexPath := libraryIndexFolderPath.Join("library_index.json")

// Download the index data
httpResponse, err := http.Get(librariesmanager.LibraryIndexURL.String())
if err != nil {
feedback.Errorf("Unable to download Library Manager index from %s: %s", err, librariesmanager.LibraryIndexURL)
os.Exit(1)
}
defer httpResponse.Body.Close()

bytes, err := ioutil.ReadAll(httpResponse.Body)
// Write the index data to file
libraryIndexFile, err := libraryIndexPath.Create()
defer libraryIndexFile.Close()
if err != nil {
panic(err)
}

err = json.Unmarshal(bytes, &libraryManagerIndex)
if err != nil {
if _, err := io.Copy(libraryIndexFile, httpResponse.Body); err != nil {
panic(err)
}

libraryManagerIndex = librariesmanager.NewLibraryManager(libraryIndexFolderPath, nil)
libraryManagerIndex.LoadIndex()
}

if misspelledWordsReplacer == nil { // The replacer only needs to be compiled once per run.
Expand Down Expand Up @@ -120,10 +132,10 @@ func SourceHeaders() []string {
return sourceHeaders
}

var libraryManagerIndex map[string]interface{}
var libraryManagerIndex *librariesmanager.LibrariesManager

// LibraryManagerIndex returns the Library Manager index data.
func LibraryManagerIndex() map[string]interface{} {
func LibraryManagerIndex() *librariesmanager.LibrariesManager {
return libraryManagerIndex
}

Expand Down
6 changes: 3 additions & 3 deletions internal/rule/ruleconfiguration/ruleconfiguration.go
Original file line number Diff line number Diff line change
Expand Up @@ -1007,15 +1007,15 @@ var configurations = []Type{
Category: "library.properties",
Subcategory: "depends field",
ID: "LP047",
Brief: "prohibited character in depends",
Brief: "invalid depends format",
Description: "",
MessageTemplate: "Prohibited character(s) in library.properties depends field {{.}}. See: https://arduino.github.io/arduino-cli/latest/library-specification/#libraryproperties-file-format",
MessageTemplate: "Invalid format of library.properties depends field {{.}}. See: https://arduino.github.io/arduino-cli/latest/library-specification/#libraryproperties-file-format",
DisableModes: nil,
EnableModes: []rulemode.Type{rulemode.Default},
InfoModes: nil,
WarningModes: nil,
ErrorModes: []rulemode.Type{rulemode.Default},
RuleFunction: rulefunction.LibraryPropertiesDependsFieldDisallowedCharacters,
RuleFunction: rulefunction.LibraryPropertiesDependsFieldInvalidFormat,
},
{
ProjectType: projecttype.Library,
Expand Down
69 changes: 49 additions & 20 deletions internal/rule/rulefunction/library.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ import (
"net/http"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/arduino/arduino-cli/arduino/libraries"
"github.com/arduino/arduino-cli/arduino/libraries/librariesindex"
"github.com/arduino/arduino-cli/arduino/utils"
"github.com/arduino/arduino-lint/internal/project/library"
"github.com/arduino/arduino-lint/internal/project/projectdata"
Expand Down Expand Up @@ -1158,8 +1160,8 @@ func LibraryPropertiesArchitecturesFieldValueCase() (result ruleresult.Type, out
return ruleresult.Pass, ""
}

// LibraryPropertiesDependsFieldDisallowedCharacters checks for disallowed characters in the library.properties "depends" field.
func LibraryPropertiesDependsFieldDisallowedCharacters() (result ruleresult.Type, output string) {
// LibraryPropertiesDependsFieldInvalidFormat checks for the library.properties "depends" field having an invalid format.
func LibraryPropertiesDependsFieldInvalidFormat() (result ruleresult.Type, output string) {
if projectdata.LibraryPropertiesLoadError() != nil {
return ruleresult.NotRun, "Couldn't load library.properties"
}
Expand Down Expand Up @@ -1187,21 +1189,55 @@ func LibraryPropertiesDependsFieldNotInIndex() (result ruleresult.Type, output s
return ruleresult.Skip, "Field not present"
}

dependencies := commaSeparatedToList(depends)
dependsList := commaSeparatedToList(depends)

dependenciesNotInIndex := []string{}
for _, dependency := range dependencies {
if dependency == "" {
var dependencyRegexp = regexp.MustCompile("^([^()]+?) *(?:\\((.+)\\))?$")
dependsNotInIndex := []string{}
for _, depend := range dependsList {
// Process raw depend string into a dependency object
if depend == "" {
// This is the responsibility of LibraryPropertiesDependsFieldInvalidFormat()
continue
}
logrus.Tracef("Checking if dependency %s is in index.", dependency)
if !nameInLibraryManagerIndex(dependency) {
dependenciesNotInIndex = append(dependenciesNotInIndex, dependency)
dependencyData := dependencyRegexp.FindAllStringSubmatch(depend, -1)
if dependencyData == nil {
// This is the responsibility of LibraryPropertiesDependsFieldInvalidFormat()
continue
}
dependencyConstraint, err := semver.ParseConstraint(dependencyData[0][2])
if err != nil {
// This is the responsibility of LibraryPropertiesDependsFieldInvalidFormat()
continue
}
var dependency semver.Dependency = &librariesindex.Dependency{
Name: dependencyData[0][1],
VersionConstraint: dependencyConstraint,
}

logrus.Tracef("Checking if dependency %s is in index.", depend)
// Get all releases of the dependency
library := projectdata.LibraryManagerIndex().Index.FindIndexedLibrary(&libraries.Library{Name: dependency.GetName()})
if library == nil {
logrus.Tracef("Dependency is not in the index.")
dependsNotInIndex = append(dependsNotInIndex, depend)
continue
}
// Convert the dependency's libraries.Library object to a semver.Releases object
var releases semver.Releases
for _, release := range library.Releases {
releases = append(releases, release)
}
// Filter the dependency's releases according to the dependency's constraint
dependencyReleases := releases.FilterBy(dependency)
if len(dependencyReleases) == 0 {
logrus.Tracef("No releases match dependency's constraint.")
dependsNotInIndex = append(dependsNotInIndex, depend)
continue
}
}

if len(dependenciesNotInIndex) > 0 {
return ruleresult.Fail, strings.Join(dependenciesNotInIndex, ", ")
if len(dependsNotInIndex) > 0 {
return ruleresult.Fail, strings.Join(dependsNotInIndex, ", ")
}

return ruleresult.Pass, ""
Expand Down Expand Up @@ -1461,15 +1497,8 @@ func IncorrectExamplesFolderNameCase() (result ruleresult.Type, output string) {

// nameInLibraryManagerIndex returns whether there is a library in Library Manager index using the given name.
func nameInLibraryManagerIndex(name string) bool {
libraries := projectdata.LibraryManagerIndex()["libraries"].([]interface{})
for _, libraryInterface := range libraries {
library := libraryInterface.(map[string]interface{})
if library["name"].(string) == name {
return true
}
}

return false
library := projectdata.LibraryManagerIndex().Index.FindIndexedLibrary(&libraries.Library{Name: name})
return library != nil
}

// spellCheckLibraryPropertiesFieldValue returns the value of the provided library.properties field with commonly misspelled words corrected.
Expand Down
11 changes: 7 additions & 4 deletions internal/rule/rulefunction/library_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -794,22 +794,25 @@ func TestLibraryPropertiesArchitecturesFieldValueCase(t *testing.T) {
checkLibraryRuleFunction(LibraryPropertiesArchitecturesFieldValueCase, testTables, t)
}

func TestLibraryPropertiesDependsFieldDisallowedCharacters(t *testing.T) {
func TestLibraryPropertiesDependsFieldInvalidFormat(t *testing.T) {
testTables := []libraryRuleFunctionTestTable{
{"Invalid", "InvalidLibraryProperties", ruleresult.NotRun, ""},
{"Legacy", "Legacy", ruleresult.Skip, ""},
{"Depends field has disallowed characters", "DependsHasBadChars", ruleresult.Fail, ""},
{"Valid", "DependsIndexed", ruleresult.Pass, ""},
{"Valid", "DependsValid", ruleresult.Pass, ""},
}

checkLibraryRuleFunction(LibraryPropertiesDependsFieldDisallowedCharacters, testTables, t)
checkLibraryRuleFunction(LibraryPropertiesDependsFieldInvalidFormat, testTables, t)
}

func TestLibraryPropertiesDependsFieldNotInIndex(t *testing.T) {
testTables := []libraryRuleFunctionTestTable{
{"Unable to load", "InvalidLibraryProperties", ruleresult.NotRun, ""},
{"Legacy", "Legacy", ruleresult.Skip, ""},
{"No depends field", "MissingFields", ruleresult.Skip, ""},
{"Dependency not in index", "DependsNotIndexed", ruleresult.Fail, "^NotIndexed$"},
{"Dependency in index", "DependsIndexed", ruleresult.Pass, ""},
{"Dependency constraint not in index", "DependsConstraintNotIndexed", ruleresult.Fail, "^Servo \\(=0\\.0\\.1\\)$"},
{"Dependencies in index", "DependsIndexed", ruleresult.Pass, ""},
{"Depends field empty", "DependsEmpty", ruleresult.Pass, ""},
{"No depends", "NoDepends", ruleresult.Skip, ""},
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name=DependsConstraintNotIndexed
version=1.0.0
author=Cristian Maglie <[email protected]>, Pippo Pluto <[email protected]>
maintainer=Cristian Maglie <[email protected]>
sentence=A library that makes coding a web server a breeze.
paragraph=Supports HTTP1.1 and you can do GET and POST.
category=Communication
url=http://example.com/
architectures=avr
depends=Servo (=0.0.1)
includes=DependsConstraintNotIndexed.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ paragraph=Supports HTTP1.1 and you can do GET and POST.
category=Communication
url=http://example.com/
architectures=avr
depends=Servo, , Adafruit NeoPixel
depends=,(foo),foo (bar),Adafruit NeoPixel,Servo (<1.1.4),Stepper (<=1.1.3),Mouse (=1.0.0),Keyboard (>=1.0.1),WiFiNINA (>1.0.0)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name=DependsValid
version=1.0.0
author=Cristian Maglie <[email protected]>, Pippo Pluto <[email protected]>
maintainer=Cristian Maglie <[email protected]>
sentence=A library that makes coding a web server a breeze.
paragraph=Supports HTTP1.1 and you can do GET and POST.
category=Communication
url=http://example.com/
architectures=avr
depends=Servo , Adafruit NeoPixel,Stepper (<1.1.3) , Mouse (<=1.0.0),Keyboard (=1.2.3-beta),Ethernet (>=2.0),WiFiNINA (>0.0.8)
includes=DependsValid.h
16 changes: 3 additions & 13 deletions internal/rule/schema/schemadata/bindata.go
Original file line number Diff line number Diff line change
Expand Up @@ -1440,13 +1440,7 @@ var _arduinoLibraryPropertiesDefinitionsSchemaJson = []byte(`{
"allowedCharacters": {
"allOf": [
{
"$ref": "#/definitions/propertiesObjects/depends/base/definitions/patternObject"
},
{
"not": {
"$comment": "The depends property is a comma separated list of names, so a valid name pattern is a valid depends pattern with the comma excluded",
"pattern": "^.*,.*$"
}
"pattern": "^(([a-zA-Z][a-zA-Z0-9 _.\\-]*)|([0-9][a-zA-Z0-9 _.\\-]*[a-zA-Z][a-zA-Z0-9 _.\\-]*))*$"
}
]
}
Expand Down Expand Up @@ -1913,18 +1907,14 @@ var _arduinoLibraryPropertiesDefinitionsSchemaJson = []byte(`{
},
"depends": {
"base": {
"definitions": {
"patternObject": {
"pattern": "^(([a-zA-Z][a-zA-Z0-9 _\\.\\-,]*)|([0-9][a-zA-Z0-9 _\\.\\-]*[a-zA-Z][a-zA-Z0-9 _\\.\\-,]*))*$"
}
},
"object": {
"allOf": [
{
"type": "string"
},
{
"$ref": "#/definitions/propertiesObjects/depends/base/definitions/patternObject"
"$comment": "Based on #/definitions/propertiesObjects/name/base/definitions/patternObjects/allowedCharacters and general-definitions-schema.json#/definitions/patternObjects/relaxedSemver",
"pattern": "^((((([a-zA-Z][a-zA-Z0-9 _.\\-]*)|([0-9][a-zA-Z0-9 _.\\-]*[a-zA-Z][a-zA-Z0-9 _.\\-]*))+( \\( *(<|<=|=|>=|>)(0|[1-9]\\d*)(\\.(0|[1-9]\\d*))?(\\.(0|[1-9]\\d*))?(-((0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(\\.(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\\+([0-9a-zA-Z-]+(\\.[0-9a-zA-Z-]+)*))? *\\) *)?), *)*((([a-zA-Z][a-zA-Z0-9 _.\\-]*)|([0-9][a-zA-Z0-9 _.\\-]*[a-zA-Z][a-zA-Z0-9 _.\\-]*))+( \\( *(<|<=|=|>=|>)(0|[1-9]\\d*)(\\.(0|[1-9]\\d*))?(\\.(0|[1-9]\\d*))?(-((0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(\\.(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\\+([0-9a-zA-Z-]+(\\.[0-9a-zA-Z-]+)*))? *\\) *)?))?$"
}
]
}
Expand Down