diff --git a/.github/workflows/coverage-generate.yaml b/.github/workflows/coverage-generate.yaml index a261c4ee..31e5ed6d 100644 --- a/.github/workflows/coverage-generate.yaml +++ b/.github/workflows/coverage-generate.yaml @@ -37,6 +37,10 @@ jobs: sudo apt install -y build-essential cppcheck doxygen graphviz npm install -g jsonld-cli npm --prefix scripts ci + - name: Install Go static analysis tools + run: | + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + go install honnef.co/go/tools/cmd/staticcheck@latest - name: Install Rust static analysis tools run: | rustup component add clippy diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b5c7f61b..344ca395 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -50,6 +50,10 @@ jobs: run: | brew update brew install cppcheck doxygen gcc go graphviz + - name: Install Go static analysis tools + run: | + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + go install honnef.co/go/tools/cmd/staticcheck@latest - name: Install Rust static analysis tools run: | rustup component add clippy diff --git a/pyproject.toml b/pyproject.toml index e58cfe30..22676237 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ dev = [ "mypy >= 1.19.1", # latest version for Python 3.9 is 1.19.x "pyrefly >= 0.55.0", "pyright >= 1.1.403", - "pyshacl >= 0.25.0", + "pyshacl >= 0.25.0, < 0.40.0", # 0.40.0 uses `str | X` at runtime, breaks on Python < 3.10 "pytest >= 7.4", "pytest-cov >= 4.1", "pytest-xdist >= 3.5", diff --git a/src/shacl2code/lang/golang.py b/src/shacl2code/lang/golang.py index ad3e3700..a10d11ec 100644 --- a/src/shacl2code/lang/golang.py +++ b/src/shacl2code/lang/golang.py @@ -242,9 +242,9 @@ def __init__(self, args): # template "go_package": textwrap.dedent(f""" */ - package {args.package} /*"""), + "package_name": args.package, } @classmethod diff --git a/src/shacl2code/lang/templates/golang/classes.go.j2 b/src/shacl2code/lang/templates/golang/classes.go.j2 index 8005cadc..fd30e78a 100644 --- a/src/shacl2code/lang/templates/golang/classes.go.j2 +++ b/src/shacl2code/lang/templates/golang/classes.go.j2 @@ -3,6 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} @@ -14,10 +15,17 @@ import ( /*{{ '*' }}{{ '/' }} {% for class in classes %} -{%- for l in class.comment.splitlines() %} -{{ ("// " + l).rstrip() }} -{% endfor -%} +{%- set class_comment_lines = class.comment.splitlines() %} +{%- if class_comment_lines %} +// {{ (struct_name(class) + " " + class_comment_lines[0]).rstrip() }} +{%- for l in class_comment_lines[1:] %} +// {{ l }} +{%- endfor %} +{%- else %} +// {{ struct_name(class) }} is a generated SHACL object type. +{%- endif %} {%- if class.deprecated %} +// // Deprecated: This class is deprecated {%- endif %} {% set is_public = not class.is_abstract or class.is_extensible -%} @@ -44,10 +52,17 @@ type {{ struct_name(class) }} struct { {%- endfor %} } {% for member in class.named_individuals %} -{%- for l in member.comment.splitlines() %} -{{ ("// " + l).rstrip() }} -{% endfor -%} -const {{ varname(*class.clsname) }}{{ varname(member.varname) }} = "{{ member._id }}" +{%- set constname = varname(*class.clsname) + varname(member.varname) %} +{%- set comment_lines = member.comment.splitlines() %} +{%- if comment_lines %} +// {{ (constname + " " + comment_lines[0]).rstrip() }} +{%- for l in comment_lines[1:] %} +// {{ l }} +{%- endfor %} +{% else %} +// {{ constname }} is a generated named individual value. +{% endif -%} +const {{ constname }} = "{{ member._id }}" {%- endfor %} type {{ struct_name(class) }}Type struct { @@ -184,7 +199,7 @@ func Make{{ varname(*class.clsname) }}Ref() Ref[{{ interface_name(class) }}] { {%- endif %} func (self *{{ struct_name(class) }}) Validate(path Path, handler ErrorHandler) bool { - var valid bool = true + var valid = true {%- if class.parent_ids %} {%- for p in class.parent_ids %} if ! self.{{ struct_name(classes.get(p)) }}.Validate(path, handler) { diff --git a/src/shacl2code/lang/templates/golang/decode.go.j2 b/src/shacl2code/lang/templates/golang/decode.go.j2 index 03aa6abb..81f925df 100644 --- a/src/shacl2code/lang/templates/golang/decode.go.j2 +++ b/src/shacl2code/lang/templates/golang/decode.go.j2 @@ -3,6 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} @@ -230,11 +231,10 @@ func DecodeInteger(data any, path Path, context map[string]string, check DecodeC return 0, err } - switch data.(type) { + switch v := data.(type) { case int: - return data.(int), nil + return v, nil case float64: - v := data.(float64) if v == float64(int64(v)) { return int(v), nil } @@ -249,15 +249,15 @@ func DecodeFloat(data any, path Path, context map[string]string, check DecodeChe return 0, err } - switch data.(type) { + switch v := data.(type) { case float64: - return data.(float64), nil + return v, nil case string: - v, err := strconv.ParseFloat(data.(string), 64) + f, err := strconv.ParseFloat(v, 64) if err != nil { return 0, err } - return v, nil + return f, nil default: return 0, &DecodeError{path, "Float expected. Got " + reflect.TypeOf(data).Name()} } diff --git a/src/shacl2code/lang/templates/golang/encode.go.j2 b/src/shacl2code/lang/templates/golang/encode.go.j2 index 09d18fab..aa1367f7 100644 --- a/src/shacl2code/lang/templates/golang/encode.go.j2 +++ b/src/shacl2code/lang/templates/golang/encode.go.j2 @@ -3,6 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} diff --git a/src/shacl2code/lang/templates/golang/errorhandler.go.j2 b/src/shacl2code/lang/templates/golang/errorhandler.go.j2 index 0e796f66..eb6e9408 100644 --- a/src/shacl2code/lang/templates/golang/errorhandler.go.j2 +++ b/src/shacl2code/lang/templates/golang/errorhandler.go.j2 @@ -3,6 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} diff --git a/src/shacl2code/lang/templates/golang/errors.go.j2 b/src/shacl2code/lang/templates/golang/errors.go.j2 index d1c4ec7c..cd4fdaa4 100644 --- a/src/shacl2code/lang/templates/golang/errors.go.j2 +++ b/src/shacl2code/lang/templates/golang/errors.go.j2 @@ -3,12 +3,13 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ -// Validation Error +// ValidationError reports a SHACL validation failure. type ValidationError struct { Property string Err string @@ -16,7 +17,7 @@ type ValidationError struct { func (e *ValidationError) Error() string { return e.Property + ": " + e.Err } -// Conversion Error +// ConversionError reports a failed reference conversion. type ConversionError struct { From string To string @@ -26,7 +27,7 @@ func (e *ConversionError) Error() string { return "Unable to convert from " + e.From + " to " + e.To } -// Decode Error +// DecodeError reports a SHACL decode failure. type DecodeError struct { Path Path Err string diff --git a/src/shacl2code/lang/templates/golang/extensible.go.j2 b/src/shacl2code/lang/templates/golang/extensible.go.j2 index dddf7ee0..c21400ca 100644 --- a/src/shacl2code/lang/templates/golang/extensible.go.j2 +++ b/src/shacl2code/lang/templates/golang/extensible.go.j2 @@ -3,6 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} @@ -34,9 +35,7 @@ func (self *SHACLExtensibleBase) EncodeExtProperties(data map[string]any, path P } lst := []any{} - for _, v := range values { - lst = append(lst, v) - } + lst = append(lst, values...) data[state.CompactIRI(k)] = lst } return nil diff --git a/src/shacl2code/lang/templates/golang/linkstate.go.j2 b/src/shacl2code/lang/templates/golang/linkstate.go.j2 index a3273c54..98e6f5d7 100644 --- a/src/shacl2code/lang/templates/golang/linkstate.go.j2 +++ b/src/shacl2code/lang/templates/golang/linkstate.go.j2 @@ -3,6 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} diff --git a/src/shacl2code/lang/templates/golang/listproperty.go.j2 b/src/shacl2code/lang/templates/golang/listproperty.go.j2 index b481a7ea..de557ce2 100644 --- a/src/shacl2code/lang/templates/golang/listproperty.go.j2 +++ b/src/shacl2code/lang/templates/golang/listproperty.go.j2 @@ -3,6 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} @@ -70,7 +71,7 @@ func (self *ListProperty[T]) Delete() { } func (self *ListProperty[T]) IsSet() bool { - return self.value != nil && len(self.value) > 0 + return len(self.value) > 0 } func (self *ListProperty[T]) Check(path Path, handler ErrorHandler) bool { diff --git a/src/shacl2code/lang/templates/golang/optional.go.j2 b/src/shacl2code/lang/templates/golang/optional.go.j2 index f7558149..24613768 100644 --- a/src/shacl2code/lang/templates/golang/optional.go.j2 +++ b/src/shacl2code/lang/templates/golang/optional.go.j2 @@ -3,6 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} diff --git a/src/shacl2code/lang/templates/golang/path.go.j2 b/src/shacl2code/lang/templates/golang/path.go.j2 index 3ca73ce8..9c31d1dc 100644 --- a/src/shacl2code/lang/templates/golang/path.go.j2 +++ b/src/shacl2code/lang/templates/golang/path.go.j2 @@ -3,6 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} diff --git a/src/shacl2code/lang/templates/golang/property.go.j2 b/src/shacl2code/lang/templates/golang/property.go.j2 index c0225db8..282f5c43 100644 --- a/src/shacl2code/lang/templates/golang/property.go.j2 +++ b/src/shacl2code/lang/templates/golang/property.go.j2 @@ -3,6 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} diff --git a/src/shacl2code/lang/templates/golang/ref.go.j2 b/src/shacl2code/lang/templates/golang/ref.go.j2 index a4fe6819..445ce20a 100644 --- a/src/shacl2code/lang/templates/golang/ref.go.j2 +++ b/src/shacl2code/lang/templates/golang/ref.go.j2 @@ -3,6 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} @@ -12,7 +13,7 @@ import ( "reflect" ) -// Reference +// Ref is a reference to a SHACL object. type Ref[T SHACLObject] interface { GetIRI() string GetObj() T @@ -79,8 +80,9 @@ func MakeIRIRef[T SHACLObject](iri string) Ref[T] { return &ref[T]{nil, iri} } -// Convert one reference to another. Note that the output type is first so it -// can be specified, while the input type is generally inferred from the argument +// ConvertRef converts a reference from one type to another. +// Note that the output type is first so it can be specified, +// while the input type is generally inferred from the argument. func ConvertRef[TO SHACLObject, FROM SHACLObject](in Ref[FROM]) (Ref[TO], error) { if in.IsObj() { out_obj, ok := any(in.GetObj()).(TO) diff --git a/src/shacl2code/lang/templates/golang/reflistproperty.go.j2 b/src/shacl2code/lang/templates/golang/reflistproperty.go.j2 index f51482d5..c4ca4130 100644 --- a/src/shacl2code/lang/templates/golang/reflistproperty.go.j2 +++ b/src/shacl2code/lang/templates/golang/reflistproperty.go.j2 @@ -3,6 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} diff --git a/src/shacl2code/lang/templates/golang/refproperty.go.j2 b/src/shacl2code/lang/templates/golang/refproperty.go.j2 index 4a32c2d6..a5486ba8 100644 --- a/src/shacl2code/lang/templates/golang/refproperty.go.j2 +++ b/src/shacl2code/lang/templates/golang/refproperty.go.j2 @@ -3,6 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} diff --git a/src/shacl2code/lang/templates/golang/shaclobject.go.j2 b/src/shacl2code/lang/templates/golang/shaclobject.go.j2 index a4e97456..aff78310 100644 --- a/src/shacl2code/lang/templates/golang/shaclobject.go.j2 +++ b/src/shacl2code/lang/templates/golang/shaclobject.go.j2 @@ -3,6 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} @@ -10,7 +11,7 @@ SPDX-License-Identifier: {{ spdx_license }} type Visit func(Path, any) -// Base SHACL Object +// SHACLObjectBase is the base type embedded by all SHACL objects. type SHACLObjectBase struct { // Object ID id Property[string] @@ -21,7 +22,7 @@ type SHACLObjectBase struct { func (self *SHACLObjectBase) ID() PropertyInterface[string] { return &self.id } func (self *SHACLObjectBase) Validate(path Path, handler ErrorHandler) bool { - var valid bool = true + var valid = true switch self.typ.GetNodeKind() { case NodeKindBlankNode: diff --git a/src/shacl2code/lang/templates/golang/shaclobjectset.go.j2 b/src/shacl2code/lang/templates/golang/shaclobjectset.go.j2 index 397f0741..b080e03a 100644 --- a/src/shacl2code/lang/templates/golang/shaclobjectset.go.j2 +++ b/src/shacl2code/lang/templates/golang/shaclobjectset.go.j2 @@ -3,6 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} @@ -173,7 +174,7 @@ func (self *SHACLObjectSetObject) Decode(decoder *json.Decoder) error { } // Check context strings. Using maps.Equal() would be much better, but requires go >= 1.21 - sort.Sort(sort.StringSlice(context_urls)) + sort.Strings(context_urls) if !reflect.DeepEqual(context_urls, CONTEXT_URLS) { return &DecodeError{sub_path, "Wrong context URL(s)"} @@ -304,7 +305,7 @@ func (self *SHACLObjectSetObject) Encode(encoder *json.Encoder) error { // Convert to a list and sort top_list := []SHACLObject{} - for o, _ := range top_objects { + for o := range top_objects { top_list = append(top_list, o) } @@ -348,9 +349,8 @@ func (self *SHACLObjectSetObject) Walk(visit Visit) { visited := map[SHACLObject]bool{} visit_proxy := func(path Path, v any) { - switch v.(type) { + switch r := v.(type) { case Ref[SHACLObject]: - r := v.(Ref[SHACLObject]) if !r.IsObj() { visit(path, v) return diff --git a/src/shacl2code/lang/templates/golang/shacltype.go.j2 b/src/shacl2code/lang/templates/golang/shacltype.go.j2 index ec28064c..dffa6cd3 100644 --- a/src/shacl2code/lang/templates/golang/shacltype.go.j2 +++ b/src/shacl2code/lang/templates/golang/shacltype.go.j2 @@ -3,6 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} diff --git a/src/shacl2code/lang/templates/golang/util.go.j2 b/src/shacl2code/lang/templates/golang/util.go.j2 index a1fc9c53..ac7deed5 100644 --- a/src/shacl2code/lang/templates/golang/util.go.j2 +++ b/src/shacl2code/lang/templates/golang/util.go.j2 @@ -3,6 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} @@ -12,7 +13,7 @@ import ( "strings" ) -// IRI Validation +// IsIRI reports whether iri is a valid IRI, not a blank node. func IsIRI(iri string) bool { if strings.HasPrefix(iri, "_:") { return false diff --git a/src/shacl2code/lang/templates/golang/validator.go.j2 b/src/shacl2code/lang/templates/golang/validator.go.j2 index 35839db2..f908a2bd 100644 --- a/src/shacl2code/lang/templates/golang/validator.go.j2 +++ b/src/shacl2code/lang/templates/golang/validator.go.j2 @@ -3,6 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} @@ -22,22 +23,21 @@ type Validator[T any] interface { } func ValueToString(val any, name string) (string, error) { - switch val.(type) { + switch v := val.(type) { case string: - return val.(string), nil + return v, nil case int: - return strconv.Itoa(val.(int)), nil + return strconv.Itoa(v), nil case time.Time: - t := val.(time.Time) - if t.Location() == time.UTC { - return strftime.Format(UtcFormatStr, t), nil + if v.Location() == time.UTC { + return strftime.Format(UtcFormatStr, v), nil } - return strftime.Format(TzFormatStr, t), nil + return strftime.Format(TzFormatStr, v), nil } return "", &ValidationError{name, "Value is of unsupported type " + reflect.TypeOf(val).Name()} } -// ID Validator +// IDValidator validates that a value is an IRI or a blank node. type IDValidator struct{} func (self IDValidator) Check(val string, name string) error { @@ -47,7 +47,7 @@ func (self IDValidator) Check(val string, name string) error { return nil } -// Regex Validator +// RegexValidator validates a value against a regex pattern. type RegexValidator[T any] struct { Regex string } @@ -68,7 +68,7 @@ func (self RegexValidator[T]) Check(val T, name string) error { return nil } -// Integer Min Validator +// IntegerMinValidator validates that a value is >= a minimum. type IntegerMinValidator struct { Min int } @@ -80,7 +80,7 @@ func (self IntegerMinValidator) Check(val int, name string) error { return nil } -// Integer Max Validator +// IntegerMaxValidator validates that a value is <= a maximum. type IntegerMaxValidator struct { Max int } @@ -92,7 +92,7 @@ func (self IntegerMaxValidator) Check(val int, name string) error { return nil } -// Enum Validator +// EnumValidator validates that a value is one of a fixed set. type EnumValidator struct { Values []string } diff --git a/tests/test_golang.py b/tests/test_golang.py index 434d75ed..08a183bc 100644 --- a/tests/test_golang.py +++ b/tests/test_golang.py @@ -1,6 +1,10 @@ # # Copyright (c) 2024 Joshua Watt # +# SPDX-FileContributor: Joshua Watt +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2024 Joshua Watt +# SPDX-FileType: SOURCE # SPDX-License-Identifier: MIT import json @@ -474,13 +478,35 @@ def link_test(test_lib, tmp_path_factory): ) -@pytest.mark.parametrize( +GOLANG_MODEL_TESTS = ( "args", [ ["--input", TEST_MODEL], ["--input", TEST_MODEL, "--context-url", TEST_CONTEXT, SPDX3_CONTEXT_URL], ], ) + + +def _build_golang_module(tmp_path, args): + subprocess.run( + [ + "shacl2code", + "generate", + ] + + args + + [ + "golang", + "--output", + tmp_path, + ], + check=True, + ) + + subprocess.run(["go", "mod", "init", "model"], cwd=tmp_path, check=True) + subprocess.run(["go", "mod", "tidy"], cwd=tmp_path, check=True) + + +@pytest.mark.parametrize(*GOLANG_MODEL_TESTS) class TestOutput: def test_trailing_whitespace(self, tmp_path, args): """ @@ -509,27 +535,46 @@ def test_trailing_whitespace(self, tmp_path, args): ), f"{fn}: Line {num + 1} has trailing whitespace: {line!r}" def test_output_compile(self, tmp_path, args): + _build_golang_module(tmp_path, args) + + model_output = tmp_path / "model.a" + subprocess.run( - [ - "shacl2code", - "generate", - ] - + args - + [ - "golang", - "--output", - tmp_path, - ], + ["go", "build", "-o", model_output, "."], + cwd=tmp_path, check=True, ) - subprocess.run(["go", "mod", "init", "model"], cwd=tmp_path, check=True) - subprocess.run(["go", "mod", "tidy"], cwd=tmp_path, check=True) - model_output = tmp_path / "model.a" +@pytest.mark.parametrize(*GOLANG_MODEL_TESTS) +class TestStaticAnalysis: + """ + Static analysis checks for the generated Go code + """ + + def test_vet(self, tmp_path, args): + """ + go vet static analysis + """ + _build_golang_module(tmp_path, args) + subprocess.run(["go", "vet", "./..."], cwd=tmp_path, check=True) + def test_staticcheck(self, tmp_path, args): + """ + staticcheck static analysis. + + Disabled: + - ST1003 (snake_case names): generated identifiers mirror the SHACL + model's own naming. + - ST1006 ("self" receiver name): generated methods use "self" to + match other language bindings. + + Any other finding fails the test so it shows up as a failed check on + the PR. + """ + _build_golang_module(tmp_path, args) subprocess.run( - ["go", "build", "-o", model_output, "."], + ["staticcheck", "-checks=all,-ST1003,-ST1006", "./..."], cwd=tmp_path, check=True, )