├── .github ├── ISSUE_TEMPLATE │ └── bug.md └── workflows │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── .mockery.yaml ├── Docker ├── base │ └── Dockerfile ├── generator │ └── Dockerfile └── lint │ └── Dockerfile ├── LICENSE ├── README.md ├── Taskfile.yml ├── cmd └── easyp │ └── main.go ├── easyp.yaml ├── example.easyp.yaml ├── go.mod ├── go.sum ├── internal ├── adapters │ ├── console │ │ ├── bash.go │ │ ├── error.go │ │ ├── new.go │ │ └── powershell.go │ ├── go_git │ │ ├── get_dir_walker.go │ │ └── go_git.go │ ├── lock_file │ │ ├── deps_iter.go │ │ ├── is_empty.go │ │ ├── lock_file.go │ │ ├── read.go │ │ └── write.go │ ├── module_config │ │ ├── module_config.go │ │ ├── read_buf_work.go │ │ ├── read_easyp.go │ │ └── read_from_repo.go │ ├── repository │ │ ├── git │ │ │ ├── archive.go │ │ │ ├── fetch.go │ │ │ ├── get_files.go │ │ │ ├── git.go │ │ │ ├── read_file.go │ │ │ └── read_revision.go │ │ └── repository.go │ └── storage │ │ ├── cache_download.go │ │ ├── create_cache_repository_dir.go │ │ ├── get_cache_download_paths.go │ │ ├── get_cache_download_paths_test.go │ │ ├── get_install_dir.go │ │ ├── get_install_dir_test.go │ │ ├── get_installed_module_hash.go │ │ ├── install.go │ │ ├── install_test.go │ │ ├── is_module_installed.go │ │ ├── is_module_installed_test.go │ │ ├── mocks │ │ └── LockFile.go │ │ ├── sanitize.go │ │ ├── storage.go │ │ └── storage_test.go ├── api │ ├── breaking_check.go │ ├── completion.go │ ├── enum.go │ ├── generate.go │ ├── init.go │ ├── interface.go │ ├── lint.go │ ├── mod.go │ └── temporaly_helper.go ├── config │ ├── breaking_check.go │ ├── config.go │ ├── default.go │ └── lint.go ├── core │ ├── breaking_check.go │ ├── breaking_checker.go │ ├── breaking_checker_test.go │ ├── check_lint_ignore.go │ ├── core.go │ ├── dom.go │ ├── download.go │ ├── fs.go │ ├── generate.go │ ├── get.go │ ├── init.go │ ├── instruction_parser.go │ ├── instruction_parser_test.go │ ├── lint.go │ ├── mocks │ │ ├── Console.go │ │ ├── CurrentProjectGitWalker.go │ │ ├── LockFile.go │ │ ├── ModuleConfig.go │ │ ├── Rule.go │ │ └── Storage.go │ ├── mod.go │ ├── models │ │ ├── cache_download_paths.go │ │ ├── errors.go │ │ ├── lock_file_info.go │ │ ├── module.go │ │ ├── module_config.go │ │ ├── module_test.go │ │ └── revision.go │ ├── path_helpers │ │ ├── is_target_path.go │ │ └── is_target_path_test.go │ ├── proto_info_read.go │ ├── update.go │ └── vendor.go ├── flags │ └── flags.go ├── fs │ ├── fs │ │ ├── adapter.go │ │ └── dir_walker.go │ └── go_git │ │ ├── adapter.go │ │ └── dir_walker.go ├── rules │ ├── builder.go │ ├── check_lint_ignore_test.go │ ├── comment_enum.go │ ├── comment_enum_test.go │ ├── comment_enum_value.go │ ├── comment_enum_value_test.go │ ├── comment_field_message.go │ ├── comment_field_message_test.go │ ├── comment_message.go │ ├── comment_message_test.go │ ├── comment_one_of.go │ ├── comment_one_of_test.go │ ├── comment_rpc.go │ ├── comment_rpc_test.go │ ├── comment_service.go │ ├── comment_service_test.go │ ├── directory_same_package.go │ ├── directory_same_package_test.go │ ├── enum_first_value_zero.go │ ├── enum_first_value_zero_test.go │ ├── enum_no_allow_alias.go │ ├── enum_no_allow_alias_test.go │ ├── enum_pascal_case.go │ ├── enum_pascal_case_test.go │ ├── enum_value_prefix.go │ ├── enum_value_prefix_test.go │ ├── enum_value_upper_snake_case.go │ ├── enum_value_upper_snake_case_test.go │ ├── enum_zero_value_suffix.go │ ├── enum_zero_value_suffix_test.go │ ├── file_lower_snake_case.go │ ├── file_lower_snake_case_test.go │ ├── import_no_public.go │ ├── import_no_public_test.go │ ├── import_no_weak.go │ ├── import_no_weak_test.go │ ├── import_used.go │ ├── import_used_test.go │ ├── init_test.go │ ├── message_field_lower_snake_case.go │ ├── message_field_lower_snake_case_test.go │ ├── message_pascal_case.go │ ├── message_pascal_case_test.go │ ├── oneof_lower_snake_case.go │ ├── oneof_lower_snake_case_test.go │ ├── package_defined.go │ ├── package_defined_test.go │ ├── package_directory_match.go │ ├── package_directory_match_test.go │ ├── package_lower_snake_case.go │ ├── package_lower_snake_case_test.go │ ├── package_no_import_cycle.go │ ├── package_same_csharp_namespace.go │ ├── package_same_csharp_namespace_test.go │ ├── package_same_directory.go │ ├── package_same_directory_test.go │ ├── package_same_go_package.go │ ├── package_same_go_package_test.go │ ├── package_same_java_multiple_files.go │ ├── package_same_java_multiple_files_test.go │ ├── package_same_java_package.go │ ├── package_same_java_package_test.go │ ├── package_same_php_namespace.go │ ├── package_same_php_namespace_test.go │ ├── package_same_ruby_package.go │ ├── package_same_ruby_package_test.go │ ├── package_same_swift_prefix.go │ ├── package_same_swift_prefix_test.go │ ├── package_version_suffix.go │ ├── package_version_suffix_test.go │ ├── protovalidate.go │ ├── rpc_no_client_streaming.go │ ├── rpc_no_client_streaming_test.go │ ├── rpc_no_server_streaming.go │ ├── rpc_no_server_streaming_test.go │ ├── rpc_pascal_case.go │ ├── rpc_pascal_case_test.go │ ├── rpc_request_response_unique.go │ ├── rpc_request_response_unique_test.go │ ├── rpc_request_standard_name.go │ ├── rpc_request_standard_name_test.go │ ├── rpc_response_standard_name.go │ ├── rpc_response_standard_name_test.go │ ├── service_pascal_case.go │ ├── service_pascal_case_test.go │ ├── service_suffix.go │ └── service_suffix_test.go └── version │ └── version.go ├── testdata ├── api │ └── session │ │ └── v1 │ │ ├── events.proto │ │ └── session.proto ├── auth │ ├── InvalidName.proto │ ├── empty_pkg.proto │ ├── queue.proto │ └── service.proto ├── breaking_check │ ├── broken │ │ ├── messages.proto │ │ └── services.proto │ ├── not_broken │ │ ├── messages.proto │ │ └── services.proto │ └── original │ │ ├── messages.proto │ │ └── services.proto ├── import_used │ ├── enums.proto │ ├── field.proto │ ├── for_one_of.proto │ ├── messages.proto │ ├── not_used.proto │ ├── options.proto │ ├── thrd_party │ │ ├── enums.proto │ │ ├── for_extends.proto │ │ ├── for_option.proto │ │ ├── messages.proto │ │ ├── options.proto │ │ └── types.proto │ ├── types.proto │ └── used.proto ├── invalid_options │ ├── queue.proto │ └── session.proto ├── invalid_pkg │ ├── queue.proto │ └── session.proto └── no_lint │ ├── no_lint_buf_comment.proto │ └── no_lint_easyp_comment.proto └── wellknownimports ├── embed.go └── google └── protobuf ├── any.proto ├── api.proto ├── compiler └── plugin.proto ├── cpp_features.proto ├── descriptor.proto ├── duration.proto ├── empty.proto ├── field_mask.proto ├── java_features.proto ├── source_context.proto ├── struct.proto ├── timestamp.proto ├── type.proto └── wrappers.proto /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: BUG 3 | about: Bug report 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: hound672, ZergsLaw 7 | 8 | --- 9 | 10 | Describe the bug 11 | A clear and concise description of what the bug is. 12 | 13 | To Reproduce 14 | Steps to reproduce the behavior: 15 | 1. Execute command '...' 16 | 2. Pass the argument '....' 17 | 3. Include the proto file '....' 18 | 4. Observe the error 19 | 20 | Expected behavior 21 | A clear and concise description of what you expected to happen. 22 | 23 | Log files 24 | If applicable, add log files to help explain your problem. 25 | 26 | Desktop (please complete the following information): 27 | - OS: e.g. Windows 10 28 | - CLI Tool version: e.g. 1.5.0 29 | - Proto Compiler version: e.g. 3.15.6 30 | - If applicable, Python/Java/etc. version: e.g. Python 3.9.1 31 | 32 | Additional context 33 | Add any other context about the problem here, such as peculiarities of the environment. 34 | 35 | Attachments 36 | If possible, attach any related files (e.g., Proto files causing the issue) to provide more context. 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Release" 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | env: 11 | GO_VERSION: "1.24" 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | 17 | - uses: actions/setup-go@v5 18 | with: 19 | go-version: ${{ env.GO_VERSION }} 20 | 21 | - name: Create release 22 | uses: goreleaser/goreleaser-action@v6 23 | with: 24 | version: latest 25 | args: release --clean --timeout=90m 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GORELEASER_AUTH_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Install Go 17 | uses: actions/setup-go@v1 18 | with: 19 | go-version: 1.22 20 | 21 | - name: Checkout repository. 22 | uses: actions/checkout@v2 23 | 24 | - name: Install Task 25 | uses: arduino/setup-task@v1 26 | with: 27 | version: 3.x 28 | repo-token: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - name: Prepare test. 31 | run: task init 32 | 33 | - name: Start test. 34 | run: task test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | certs 3 | .env 4 | bin 5 | easyp 6 | coverage.out 7 | easyp.lock 8 | .DS_Store 9 | dist/ 10 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - staticcheck 5 | -------------------------------------------------------------------------------- /.mockery.yaml: -------------------------------------------------------------------------------- 1 | with-expecter: true 2 | quiet: false 3 | keeptree: false 4 | inpackage: false 5 | disable-version-string: true 6 | -------------------------------------------------------------------------------- /Docker/base/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=${BUILDPLATFORM} golang:1.23.3-alpine3.20 AS builder 2 | 3 | LABEL stage=gobuilder 4 | 5 | ENV CGO_ENABLED 0 6 | 7 | ENV GOOS linux 8 | 9 | RUN apk update --no-cache 10 | 11 | WORKDIR /build 12 | 13 | COPY go.mod . 14 | 15 | COPY go.sum . 16 | 17 | RUN go mod download 18 | 19 | COPY . . 20 | 21 | RUN go build -o /easyp ./cmd/easyp 22 | 23 | FROM alpine:3.20.0 24 | 25 | RUN apk update --no-cache && apk add --no-cache ca-certificates=20240226-r0 26 | 27 | COPY --from=builder /easyp /easyp 28 | -------------------------------------------------------------------------------- /Docker/generator/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG EASYP_BASE_VERSION 2 | 3 | FROM easyp/base:${EASYP_BASE_VERSION} 4 | 5 | ENV DIR_PATH="/app" 6 | ENV CONFIG_PATH="/app/easyp.yaml" 7 | 8 | CMD ["/easyp", "--cfg", "${CONFIG_PATH}", "generate", "-p", "${DIR_PATH}"] 9 | -------------------------------------------------------------------------------- /Docker/lint/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG EASYP_BASE_VERSION 2 | 3 | FROM --platform=${BUILDPLATFORM} easyp/base:${EASYP_BASE_VERSION} 4 | 5 | ENV DIR_PATH="/app" 6 | ENV CONFIG_PATH="/app/easyp.yaml" 7 | 8 | CMD ["/easyp", "--cfg", "${CONFIG_PATH}", "lint", "-p", "${DIR_PATH}"] 9 | -------------------------------------------------------------------------------- /cmd/easyp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "log/slog" 6 | "os" 7 | 8 | "github.com/samber/lo" 9 | "github.com/urfave/cli/v2" 10 | 11 | "github.com/easyp-tech/easyp/internal/api" 12 | "github.com/easyp-tech/easyp/internal/flags" 13 | "github.com/easyp-tech/easyp/internal/version" 14 | ) 15 | 16 | func initLogger(isDebug bool) *slog.Logger { 17 | // use info as default level 18 | level := slog.LevelInfo 19 | 20 | if isDebug { 21 | level = slog.LevelDebug 22 | } 23 | 24 | logger := slog.New( 25 | slog.NewTextHandler( 26 | os.Stderr, 27 | &slog.HandlerOptions{ 28 | AddSource: false, 29 | Level: level, 30 | }, 31 | ), 32 | ) 33 | 34 | slog.SetDefault(logger) // TODO: remove global state 35 | 36 | return logger 37 | } 38 | 39 | func main() { 40 | app := &cli.App{ 41 | Name: "easyp", 42 | HelpName: "easyp", 43 | Usage: "usage info", 44 | UsageText: "usage text info", 45 | ArgsUsage: "args usage info", 46 | Version: version.System(), 47 | Description: "description info", 48 | Commands: buildCommand( 49 | api.Lint{}, 50 | api.Mod{}, 51 | api.Completion{}, 52 | api.Init{}, 53 | api.Generate{}, 54 | api.BreakingCheck{}, 55 | ), 56 | Flags: []cli.Flag{ 57 | flags.Config, 58 | flags.DebugMode, 59 | }, 60 | Before: func(ctx *cli.Context) error { 61 | initLogger(ctx.Bool(flags.DebugMode.Name)) 62 | return nil 63 | }, 64 | EnableBashCompletion: true, 65 | } 66 | 67 | if err := app.Run(os.Args); err != nil { 68 | log.Fatal(err) 69 | } 70 | } 71 | 72 | func buildCommand(handlers ...api.Handler) []*cli.Command { 73 | return lo.Map(handlers, func(handler api.Handler, _ int) *cli.Command { 74 | return handler.Command() 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /easyp.yaml: -------------------------------------------------------------------------------- 1 | version: v1alpha 2 | 3 | lint: 4 | 5 | use: 6 | # Minimal 7 | - DIRECTORY_SAME_PACKAGE 8 | - PACKAGE_DEFINED 9 | - PACKAGE_DIRECTORY_MATCH 10 | - PACKAGE_SAME_DIRECTORY 11 | 12 | # Basic 13 | - ENUM_FIRST_VALUE_ZERO 14 | - ENUM_NO_ALLOW_ALIAS 15 | - ENUM_PASCAL_CASE 16 | - ENUM_VALUE_UPPER_SNAKE_CASE 17 | - FIELD_LOWER_SNAKE_CASE 18 | - IMPORT_NO_PUBLIC 19 | - IMPORT_NO_WEAK 20 | - IMPORT_USED 21 | - MESSAGE_PASCAL_CASE 22 | - ONEOF_LOWER_SNAKE_CASE 23 | - PACKAGE_LOWER_SNAKE_CASE 24 | - PACKAGE_SAME_CSHARP_NAMESPACE 25 | - PACKAGE_SAME_GO_PACKAGE 26 | - PACKAGE_SAME_JAVA_MULTIPLE_FILES 27 | - PACKAGE_SAME_JAVA_PACKAGE 28 | - PACKAGE_SAME_PHP_NAMESPACE 29 | - PACKAGE_SAME_RUBY_PACKAGE 30 | - PACKAGE_SAME_SWIFT_PREFIX 31 | - RPC_PASCAL_CASE 32 | - SERVICE_PASCAL_CASE 33 | 34 | # Default 35 | - ENUM_VALUE_PREFIX 36 | - ENUM_ZERO_VALUE_SUFFIX 37 | - FILE_LOWER_SNAKE_CASE 38 | - RPC_REQUEST_RESPONSE_UNIQUE 39 | - RPC_REQUEST_STANDARD_NAME 40 | - RPC_RESPONSE_STANDARD_NAME 41 | - PACKAGE_VERSION_SUFFIX 42 | - SERVICE_SUFFIX 43 | 44 | # Comments 45 | - COMMENT_ENUM 46 | - COMMENT_ENUM_VALUE 47 | - COMMENT_FIELD 48 | - COMMENT_MESSAGE 49 | - COMMENT_ONEOF 50 | - COMMENT_RPC 51 | - COMMENT_SERVICE 52 | 53 | # Unary RPC 54 | - RPC_NO_CLIENT_STREAMING 55 | - RPC_NO_SERVER_STREAMING 56 | 57 | enum_zero_value_suffix: NONE 58 | 59 | service_suffix: API 60 | 61 | deps: 62 | - github.com/googleapis/googleapis 63 | - github.com/bufbuild/protovalidate@v0.3.1 64 | - github.com/grpc-ecosystem/grpc-gateway@v2.19.1 65 | 66 | generate: 67 | plugins: 68 | - name: go 69 | out: . 70 | opts: 71 | paths: source_relative 72 | - name: go-grpc 73 | out: . 74 | opts: 75 | paths: source_relative 76 | require_unimplemented_servers: false 77 | - name: grpc-gateway 78 | out: . 79 | opts: 80 | paths: source_relative 81 | - name: openapiv2 82 | out: . 83 | opts: 84 | simple_operation_ids: false 85 | generate_unbound_methods: false 86 | - name: validate-go 87 | out: . 88 | opts: 89 | paths: source_relative -------------------------------------------------------------------------------- /example.easyp.yaml: -------------------------------------------------------------------------------- 1 | version: v1alpha 2 | lint: 3 | use: 4 | - COMMENT_ENUM 5 | - COMMENT_ONEOF 6 | - COMMENT_RPC 7 | - COMMENT_SERVICE 8 | - IMPORT_USED 9 | deps: 10 | - github.com/googleapis/googleapis 11 | - github.com/bufbuild/protovalidate@v0.3.1 12 | - github.com/grpc-ecosystem/grpc-gateway@v2.19.1 13 | 14 | breaking: 15 | ignore: 16 | - some_dir 17 | against_git_ref: master 18 | 19 | generate: 20 | inputs: 21 | - directory: "proto" 22 | - git_repo: 23 | url: "github.com/sipki-tech/currency@v0.1.2" 24 | plugins: 25 | - name: go 26 | out: . 27 | opts: 28 | paths: source_relative 29 | - name: go-grpc 30 | out: . 31 | opts: 32 | paths: source_relative 33 | require_unimplemented_servers: false 34 | - name: grpc-gateway 35 | out: . 36 | opts: 37 | paths: source_relative 38 | - name: openapiv2 39 | out: . 40 | opts: 41 | simple_operation_ids: false 42 | generate_unbound_methods: false 43 | - name: validate-go 44 | out: . 45 | opts: 46 | paths: source_relative 47 | 48 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/easyp-tech/easyp 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/brianvoe/gofakeit/v6 v6.28.0 7 | github.com/codeclysm/extract/v3 v3.1.1 8 | github.com/go-git/go-git/v5 v5.16.0 9 | github.com/otiai10/copy v1.14.1 10 | github.com/samber/lo v1.50.0 11 | github.com/stretchr/testify v1.10.0 12 | github.com/urfave/cli/v2 v2.27.6 13 | github.com/yoheimuta/go-protoparser/v4 v4.14.1 14 | golang.org/x/mod v0.24.0 15 | gopkg.in/yaml.v3 v3.0.1 16 | ) 17 | 18 | require ( 19 | dario.cat/mergo v1.0.1 // indirect 20 | github.com/Microsoft/go-winio v0.6.2 // indirect 21 | github.com/ProtonMail/go-crypto v1.2.0 // indirect 22 | github.com/cloudflare/circl v1.6.1 // indirect 23 | github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect 24 | github.com/cyphar/filepath-securejoin v0.4.1 // indirect 25 | github.com/davecgh/go-spew v1.1.1 // indirect 26 | github.com/emirpasic/gods v1.18.1 // indirect 27 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 28 | github.com/go-git/go-billy/v5 v5.6.2 // indirect 29 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 30 | github.com/h2non/filetype v1.1.3 // indirect 31 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 32 | github.com/juju/errors v1.0.0 // indirect 33 | github.com/kevinburke/ssh_config v1.2.0 // indirect 34 | github.com/klauspost/compress v1.18.0 // indirect 35 | github.com/otiai10/mint v1.6.3 // indirect 36 | github.com/pjbgf/sha1cd v0.3.2 // indirect 37 | github.com/pmezard/go-difflib v1.0.0 // indirect 38 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 39 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 40 | github.com/skeema/knownhosts v1.3.1 // indirect 41 | github.com/stretchr/objx v0.5.2 // indirect 42 | github.com/ulikunitz/xz v0.5.12 // indirect 43 | github.com/xanzy/ssh-agent v0.3.3 // indirect 44 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 45 | golang.org/x/crypto v0.37.0 // indirect 46 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect 47 | golang.org/x/net v0.39.0 // indirect 48 | golang.org/x/sync v0.13.0 // indirect 49 | golang.org/x/sys v0.32.0 // indirect 50 | golang.org/x/text v0.24.0 // indirect 51 | gopkg.in/warnings.v0 v0.1.2 // indirect 52 | ) 53 | -------------------------------------------------------------------------------- /internal/adapters/console/bash.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "os/exec" 7 | "strings" 8 | ) 9 | 10 | // bash provide to bash terminal. 11 | type bash struct{} 12 | 13 | // RunCmd shell command. 14 | func (bash) RunCmd(ctx context.Context, dir string, command string, commandParams ...string) (string, error) { 15 | var stderr bytes.Buffer 16 | var stdout bytes.Buffer 17 | 18 | fullCommand := append([]string{command}, commandParams...) 19 | cmd := exec.CommandContext(ctx, "bash", "-c", strings.Join(fullCommand, " ")) 20 | cmd.Dir = dir 21 | cmd.Stderr = &stderr 22 | cmd.Stdout = &stdout 23 | 24 | err := cmd.Run() 25 | if err != nil { 26 | return "", &RunError{ 27 | Command: command, 28 | CommandParams: commandParams, 29 | Dir: dir, 30 | Err: err, 31 | Stderr: stderr.String(), 32 | } 33 | } 34 | 35 | return stdout.String(), nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/adapters/console/error.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type RunError struct { 8 | Command string 9 | CommandParams []string 10 | Dir string 11 | Err error 12 | Stderr string 13 | } 14 | 15 | func (e RunError) Error() string { 16 | // TODO: extend error output 17 | return fmt.Sprintf("Command: %s; Err: %v; Stderr: %s", e.Command, e.Err, e.Stderr) 18 | } 19 | -------------------------------------------------------------------------------- /internal/adapters/console/new.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "runtime" 5 | 6 | "github.com/easyp-tech/easyp/internal/core" 7 | ) 8 | 9 | // New create new console. 10 | func New() core.Console { 11 | if runtime.GOOS == "windows" { 12 | return powershell{} 13 | } 14 | return bash{} 15 | } 16 | -------------------------------------------------------------------------------- /internal/adapters/console/powershell.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "os/exec" 7 | "strings" 8 | ) 9 | 10 | // powershell provides access to PowerShell terminal. 11 | type powershell struct{} 12 | 13 | // RunCmd executes a shell command. 14 | func (powershell) RunCmd(ctx context.Context, dir string, command string, commandParams ...string) (string, error) { 15 | var stderr bytes.Buffer 16 | var stdout bytes.Buffer 17 | 18 | fullCommand := append([]string{command}, commandParams...) 19 | cmd := exec.CommandContext(ctx, "powershell", "-NoProfile", "-NonInteractive", "-Command", strings.Join(fullCommand, " ")) 20 | cmd.Dir = dir 21 | cmd.Stderr = &stderr 22 | cmd.Stdout = &stdout 23 | 24 | err := cmd.Run() 25 | if err != nil { 26 | return "", &RunError{ 27 | Command: command, 28 | CommandParams: commandParams, 29 | Dir: dir, 30 | Err: err, 31 | Stderr: stderr.String(), 32 | } 33 | } 34 | 35 | return stdout.String(), nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/adapters/go_git/get_dir_walker.go: -------------------------------------------------------------------------------- 1 | package go_git 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/go-git/go-git/v5" 8 | "github.com/go-git/go-git/v5/plumbing" 9 | 10 | "github.com/easyp-tech/easyp/internal/core" 11 | "github.com/easyp-tech/easyp/internal/fs/go_git" 12 | ) 13 | 14 | func (g *GoGit) GetDirWalker(workingDir, gitRef, path string) (core.DirWalker, error) { 15 | repository, err := git.PlainOpen(workingDir) 16 | if err != nil { 17 | if errors.Is(err, git.ErrRepositoryNotExists) { 18 | return nil, core.ErrRepositoryDoesNotExist 19 | } 20 | 21 | return nil, fmt.Errorf("git.PlainOpen: %w", err) 22 | } 23 | refName := plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", gitRef)) 24 | 25 | refAgainst, err := repository.Reference(refName, false) 26 | if err != nil { 27 | return nil, &core.GitRefNotFoundError{GitRef: gitRef} 28 | } 29 | 30 | commitAgainst, err := repository.CommitObject(refAgainst.Hash()) 31 | if err != nil { 32 | return nil, fmt.Errorf("repository.CommitObject: %w", err) 33 | } 34 | 35 | treeAgainst, err := commitAgainst.Tree() 36 | if err != nil { 37 | return nil, fmt.Errorf("commitAgainst.Tree: %w", err) 38 | } 39 | 40 | gitTreeWalker := go_git.NewGitTreeWalker(treeAgainst, path) 41 | return gitTreeWalker, nil 42 | } 43 | -------------------------------------------------------------------------------- /internal/adapters/go_git/go_git.go: -------------------------------------------------------------------------------- 1 | package go_git 2 | 3 | type GoGit struct{} 4 | 5 | func New() *GoGit { 6 | return &GoGit{} 7 | } 8 | -------------------------------------------------------------------------------- /internal/adapters/lock_file/deps_iter.go: -------------------------------------------------------------------------------- 1 | package lockfile 2 | 3 | import ( 4 | "iter" 5 | 6 | "github.com/easyp-tech/easyp/internal/core/models" 7 | ) 8 | 9 | func (l *LockFile) DepsIter() iter.Seq[models.LockFileInfo] { 10 | return func(yield func(models.LockFileInfo) bool) { 11 | for moduleName, fileInfo := range l.cache { 12 | lockFileInfo := models.LockFileInfo{ 13 | Name: moduleName, 14 | Version: fileInfo.version, 15 | Hash: models.ModuleHash(fileInfo.hash), 16 | } 17 | if !yield(lockFileInfo) { 18 | return 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/adapters/lock_file/is_empty.go: -------------------------------------------------------------------------------- 1 | package lockfile 2 | 3 | // IsEmpty check if lock file doesn't have any deps 4 | func (l *LockFile) IsEmpty() bool { 5 | return len(l.cache) == 0 6 | } 7 | -------------------------------------------------------------------------------- /internal/adapters/lock_file/lock_file.go: -------------------------------------------------------------------------------- 1 | package lockfile 2 | 3 | import ( 4 | "bufio" 5 | "strings" 6 | 7 | "github.com/easyp-tech/easyp/internal/core" 8 | ) 9 | 10 | const ( 11 | lockFileName = "easyp.lock" 12 | ) 13 | 14 | type fileInfo struct { 15 | version string 16 | hash string 17 | } 18 | 19 | type LockFile struct { 20 | dirWalker core.DirWalker 21 | cache map[string]fileInfo 22 | } 23 | 24 | func New(dirWalker core.DirWalker) *LockFile { 25 | cache := make(map[string]fileInfo) 26 | 27 | fp, err := dirWalker.Open(lockFileName) 28 | if err == nil { 29 | fscanner := bufio.NewScanner(fp) 30 | 31 | for fscanner.Scan() { 32 | parts := strings.Fields(fscanner.Text()) 33 | if len(parts) != 3 { 34 | continue 35 | } 36 | 37 | fileInfo := fileInfo{ 38 | version: parts[1], 39 | hash: parts[2], 40 | } 41 | cache[parts[0]] = fileInfo 42 | } 43 | } 44 | 45 | lockFile := &LockFile{ 46 | dirWalker: dirWalker, 47 | cache: cache, 48 | } 49 | return lockFile 50 | } 51 | -------------------------------------------------------------------------------- /internal/adapters/lock_file/read.go: -------------------------------------------------------------------------------- 1 | package lockfile 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core/models" 5 | ) 6 | 7 | // Read information about module by its name from lock file 8 | // github.com/grpc-ecosystem/grpc-gateway v0.0.0-20240502030614-85850831b7bad2b8b60cb09783d8095176f22d98 h1:hRu1vxAH6CVNmz12mpqKue5HVBQP2neoaM/q2DLm0i4= 9 | func (l *LockFile) Read(moduleName string) (models.LockFileInfo, error) { 10 | fileInfo, ok := l.cache[moduleName] 11 | if !ok { 12 | return models.LockFileInfo{}, models.ErrModuleNotFoundInLockFile 13 | } 14 | 15 | lockFileInfo := models.LockFileInfo{ 16 | Name: moduleName, 17 | Version: fileInfo.version, 18 | Hash: models.ModuleHash(fileInfo.hash), 19 | } 20 | return lockFileInfo, nil 21 | } 22 | -------------------------------------------------------------------------------- /internal/adapters/lock_file/write.go: -------------------------------------------------------------------------------- 1 | package lockfile 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "github.com/easyp-tech/easyp/internal/core/models" 8 | ) 9 | 10 | func (l *LockFile) Write( 11 | moduleName string, revisionVersion string, installedPackageHash models.ModuleHash, 12 | ) error { 13 | fp, err := l.dirWalker.Create(lockFileName) 14 | if err != nil { 15 | return fmt.Errorf("l.dirWalker.Create: %w", err) 16 | } 17 | 18 | fileInfo := fileInfo{ 19 | version: revisionVersion, 20 | hash: string(installedPackageHash), 21 | } 22 | 23 | l.cache[moduleName] = fileInfo 24 | 25 | keys := make([]string, 0, len(l.cache)) 26 | for k := range l.cache { 27 | keys = append(keys, k) 28 | } 29 | sort.Strings(keys) 30 | 31 | for _, k := range keys { 32 | r := fmt.Sprintf("%s %s %s\n", k, l.cache[k].version, l.cache[k].hash) 33 | _, _ = fp.Write([]byte(r)) 34 | } 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/adapters/module_config/module_config.go: -------------------------------------------------------------------------------- 1 | package moduleconfig 2 | 3 | type ( 4 | // ModuleConfig implement module config logic such as buf dirs config etc 5 | ModuleConfig struct { 6 | } 7 | ) 8 | 9 | func New() *ModuleConfig { 10 | return &ModuleConfig{} 11 | } 12 | -------------------------------------------------------------------------------- /internal/adapters/module_config/read_buf_work.go: -------------------------------------------------------------------------------- 1 | package moduleconfig 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log/slog" 8 | "strings" 9 | 10 | "gopkg.in/yaml.v3" 11 | 12 | "github.com/easyp-tech/easyp/internal/adapters/repository" 13 | "github.com/easyp-tech/easyp/internal/core/models" 14 | ) 15 | 16 | type bufWork struct { 17 | Directories []string `yaml:"directories"` 18 | } 19 | 20 | const ( 21 | bufWorkFile = "buf.work.yaml" 22 | ) 23 | 24 | func readBufWork(ctx context.Context, repo repository.Repo, revision models.Revision) (bufWork, error) { 25 | content, err := repo.ReadFile(ctx, revision, bufWorkFile) 26 | if err != nil { 27 | if errors.Is(err, models.ErrFileNotFound) { 28 | slog.Debug("buf config not found") 29 | return bufWork{}, nil 30 | } 31 | return bufWork{}, fmt.Errorf("repo.ReadFile: %w", err) 32 | } 33 | 34 | buf := bufWork{} 35 | if err := yaml.NewDecoder(strings.NewReader(content)).Decode(&buf); err != nil { 36 | return bufWork{}, fmt.Errorf("yaml.NewDecoder: %w", err) 37 | } 38 | 39 | return buf, nil 40 | } 41 | -------------------------------------------------------------------------------- /internal/adapters/module_config/read_easyp.go: -------------------------------------------------------------------------------- 1 | package moduleconfig 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log/slog" 8 | "strings" 9 | 10 | "github.com/easyp-tech/easyp/internal/adapters/repository" 11 | "github.com/easyp-tech/easyp/internal/config" 12 | "github.com/easyp-tech/easyp/internal/core/models" 13 | 14 | "gopkg.in/yaml.v3" 15 | ) 16 | 17 | // Config is the configuration of easyp. 18 | // FIXME: do not duplicate of struct 19 | // but if now will import from config -> cycles deps 20 | type easypConfig struct { 21 | // Deps is the dependencies repositories 22 | Deps []string `json:"deps" yaml:"deps"` 23 | } 24 | 25 | // readEasyp read easyp's config from repository 26 | func readEasyp(ctx context.Context, repo repository.Repo, revision models.Revision) ([]models.Module, error) { 27 | content, err := repo.ReadFile(ctx, revision, config.DefaultFileName) 28 | if err != nil { 29 | if errors.Is(err, models.ErrFileNotFound) { 30 | slog.Debug("easyp config not found") 31 | return nil, nil 32 | } 33 | return nil, fmt.Errorf("repo.ReadFile: %w", err) 34 | } 35 | 36 | easyp := &easypConfig{} 37 | if err := yaml.NewDecoder(strings.NewReader(content)).Decode(&easyp); err != nil { 38 | return nil, fmt.Errorf("yaml.NewDecoder: %w", err) 39 | } 40 | 41 | modules := make([]models.Module, 0, len(easyp.Deps)) 42 | for _, dep := range easyp.Deps { 43 | module := models.NewModule(dep) 44 | modules = append(modules, module) 45 | } 46 | 47 | return modules, nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/adapters/module_config/read_from_repo.go: -------------------------------------------------------------------------------- 1 | package moduleconfig 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/easyp-tech/easyp/internal/adapters/repository" 8 | "github.com/easyp-tech/easyp/internal/core/models" 9 | ) 10 | 11 | // Read and return module's config from repository 12 | func (c *ModuleConfig) ReadFromRepo( 13 | ctx context.Context, repo repository.Repo, revision models.Revision, 14 | ) (models.ModuleConfig, error) { 15 | buf, err := readBufWork(ctx, repo, revision) 16 | if err != nil { 17 | return models.ModuleConfig{}, fmt.Errorf("readBufWork: %w", err) 18 | } 19 | 20 | modules, err := readEasyp(ctx, repo, revision) 21 | if err != nil { 22 | return models.ModuleConfig{}, fmt.Errorf("readEasyp: %w", err) 23 | } 24 | 25 | moduleConfig := models.ModuleConfig{ 26 | Directories: buf.Directories, 27 | Dependencies: modules, 28 | } 29 | return moduleConfig, nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/adapters/repository/git/archive.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/easyp-tech/easyp/internal/core/models" 8 | ) 9 | 10 | func (r *gitRepo) Archive( 11 | ctx context.Context, revision models.Revision, cacheDownloadPaths models.CacheDownloadPaths, 12 | ) error { 13 | params := []string{ 14 | "archive", "--format=zip", revision.CommitHash, "-o", cacheDownloadPaths.ArchiveFile, "*.proto", 15 | } 16 | 17 | if _, err := r.console.RunCmd(ctx, r.cacheDir, "git", params...); err != nil { 18 | return fmt.Errorf("utils.RunCmd: %w", err) 19 | } 20 | 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /internal/adapters/repository/git/fetch.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/easyp-tech/easyp/internal/core/models" 8 | ) 9 | 10 | func (r *gitRepo) Fetch(ctx context.Context, revision models.Revision) error { 11 | _, err := r.console.RunCmd( 12 | ctx, r.cacheDir, "git", "fetch", "-f", "origin", "--depth=1", revision.CommitHash, 13 | ) 14 | if err != nil { 15 | return fmt.Errorf("adapters.RunCmd (fetch): %w", err) 16 | } 17 | 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /internal/adapters/repository/git/get_files.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/easyp-tech/easyp/internal/core/models" 9 | ) 10 | 11 | func (r *gitRepo) GetFiles(ctx context.Context, revision models.Revision, dirs ...string) ([]string, error) { 12 | params := []string{ 13 | "ls-tree", "-r", revision.CommitHash, 14 | } 15 | params = append(params, dirs...) 16 | res, err := r.console.RunCmd(ctx, r.cacheDir, "git", params...) 17 | if err != nil { 18 | return nil, fmt.Errorf("utils.RunCmd: %w", err) 19 | } 20 | 21 | stats := strings.Split(res, "\n") 22 | 23 | files := make([]string, 0, len(stats)) 24 | for _, stat := range stats { 25 | stat := stat 26 | s := strings.Fields(stat) 27 | if len(s) != 4 { 28 | // TODO: write debug log that len is wrong 29 | continue 30 | } 31 | files = append(files, s[3]) 32 | } 33 | 34 | return files, nil 35 | } 36 | -------------------------------------------------------------------------------- /internal/adapters/repository/git/git.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/easyp-tech/easyp/internal/adapters/repository" 10 | ) 11 | 12 | var _ repository.Repo = (*gitRepo)(nil) 13 | 14 | // gitRepo implements repository.Repo interface 15 | type gitRepo struct { 16 | // remoteURL full repository remoteURL address with schema 17 | remoteURL string 18 | // cacheDir local cache directory for store repository 19 | cacheDir string 20 | // console for call external commands 21 | console Console 22 | } 23 | 24 | const ( 25 | // for omitted package version. HEAD is git key word. 26 | gitLatestVersionRef = "HEAD" 27 | // tag prefix on output of ls-remote command 28 | gitRefsTagPrefix = "refs/tags/" 29 | ) 30 | 31 | // Some links from go mod: 32 | // cmd/go/internal/modfetch/codehost/git.go:65 - create work dir 33 | // cmd/go/internal/modfetch/codehost/git.go:137 - git's struct 34 | 35 | // Console temporary interface for console commands, must be replaced from core.Console. 36 | type Console interface { 37 | RunCmd(ctx context.Context, dir string, command string, commandParams ...string) (string, error) 38 | } 39 | 40 | // New returns gitRepo instance 41 | // remote: full remoteURL address without schema 42 | func New(ctx context.Context, remote string, cacheDir string, console Console) (repository.Repo, error) { 43 | r := &gitRepo{ 44 | remoteURL: getRemote(remote), 45 | cacheDir: cacheDir, 46 | console: console, 47 | } 48 | 49 | if _, err := os.Stat(filepath.Join(r.cacheDir, "objects")); err == nil { 50 | // repo is already exists 51 | return r, nil 52 | } 53 | 54 | if _, err := r.console.RunCmd(ctx, r.cacheDir, "git", "init", "--bare"); err != nil { 55 | return nil, fmt.Errorf("adapters.RunCmd (init): %w", err) 56 | } 57 | 58 | _, err := r.console.RunCmd(ctx, r.cacheDir, "git", "remote", "add", "origin", r.remoteURL) 59 | if err != nil { 60 | return nil, fmt.Errorf("adapters.RunCmd (add origin): %w", err) 61 | } 62 | 63 | return r, nil 64 | } 65 | 66 | func getRemote(name string) string { 67 | return "https://" + name 68 | } 69 | -------------------------------------------------------------------------------- /internal/adapters/repository/git/read_file.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/easyp-tech/easyp/internal/core/models" 7 | ) 8 | 9 | func (r *gitRepo) ReadFile(ctx context.Context, revision models.Revision, fileName string) (string, error) { 10 | // g cat-file -p 8074ae2f42417345ef103d83fb62e4245010715d:buf.work.yaml 11 | fileRequest := revision.CommitHash + ":" + fileName 12 | content, err := r.console.RunCmd( 13 | ctx, r.cacheDir, "git", "cat-file", "-p", fileRequest, 14 | ) 15 | if err != nil { 16 | // It's too dificult to parse stderr from git 17 | // so decided that there is no file in that case 18 | return "", models.ErrFileNotFound 19 | } 20 | 21 | return content, nil 22 | } 23 | -------------------------------------------------------------------------------- /internal/adapters/repository/repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/easyp-tech/easyp/internal/core/models" 7 | ) 8 | 9 | type Repo interface { 10 | // GetFiles returns list of all files in repository 11 | GetFiles(ctx context.Context, revision models.Revision, dirs ...string) ([]string, error) 12 | 13 | // ReadFile returns file's content from repository 14 | ReadFile(ctx context.Context, revision models.Revision, fileName string) (string, error) 15 | 16 | // Archive passed storage to archive and return full path to archive 17 | Archive( 18 | ctx context.Context, revision models.Revision, cacheDownloadPaths models.CacheDownloadPaths, 19 | ) error 20 | 21 | // ReadRevision reads commit's revision by passed version 22 | // or return the latest commit if version is empty 23 | ReadRevision(ctx context.Context, requestedVersion models.RequestedVersion) (models.Revision, error) 24 | 25 | // Fetch from remote repository specified version 26 | Fetch(ctx context.Context, revision models.Revision) error 27 | } 28 | -------------------------------------------------------------------------------- /internal/adapters/storage/cache_download.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/easyp-tech/easyp/internal/core/models" 8 | ) 9 | 10 | // CacheDownload create path to downloaded cache. 11 | // Like $GOPATH/pkg/mod/cache/download 12 | func (s *Storage) CreateCacheDownloadDir(cacheDownloadPaths models.CacheDownloadPaths) error { 13 | if err := os.MkdirAll(cacheDownloadPaths.CacheDownloadDir, dirPerm); err != nil { 14 | return fmt.Errorf("os.MkdirAll: %w", err) 15 | } 16 | 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /internal/adapters/storage/create_cache_repository_dir.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "crypto/sha256" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | // CacheDir create and return path to cache dir. 11 | // cache dir contains repository cache for repository with proto files. 12 | // cmd/go/internal/modfetch/codehost/codehost.go: 228 - create workdir 13 | func (s *Storage) CreateCacheRepositoryDir(name string) (string, error) { 14 | cacheDir := filepath.Join(s.rootDir, cacheDir, fmt.Sprintf("%x", sha256.Sum256([]byte(name)))) 15 | 16 | if err := os.MkdirAll(cacheDir, dirPerm); err != nil { 17 | return "", fmt.Errorf("os.MkdirAll: %w", err) 18 | } 19 | 20 | return cacheDir, nil 21 | } 22 | -------------------------------------------------------------------------------- /internal/adapters/storage/get_cache_download_paths.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/easyp-tech/easyp/internal/core/models" 7 | ) 8 | 9 | // GetDownloadArchivePath returns full path to download archive (include extension) 10 | func (s *Storage) GetCacheDownloadPaths(module models.Module, revision models.Revision) models.CacheDownloadPaths { 11 | cacheDownloadDir := filepath.Join(s.rootDir, cacheDir, cacheDownloadDir, module.Name) 12 | 13 | fileName := sanitizePath(revision.Version) 14 | 15 | archiveFile := filepath.Join(cacheDownloadDir, fileName) + ".zip" 16 | archiveHashFile := filepath.Join(cacheDownloadDir, fileName) + ".ziphash" 17 | moduleInfoFile := filepath.Join(cacheDownloadDir, fileName) + ".info" 18 | 19 | return models.CacheDownloadPaths{ 20 | CacheDownloadDir: cacheDownloadDir, 21 | ArchiveFile: archiveFile, 22 | ArchiveHashFile: archiveHashFile, 23 | ModuleInfoFile: moduleInfoFile, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/adapters/storage/get_cache_download_paths_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "path/filepath" 5 | ) 6 | 7 | func (s *storageSuite) Test_GetCacheDownloadPaths() { 8 | module := getFakeModule() 9 | revision := getFakeRevision() 10 | 11 | // ref values 12 | expectedCacheDownloadDir := filepath.Join(s.rootDir, cacheDir, cacheDownloadDir, module.Name) 13 | expectedArchiveFile := filepath.Join(expectedCacheDownloadDir, revision.Version) + ".zip" 14 | expectedArchiveHashFile := filepath.Join(expectedCacheDownloadDir, revision.Version) + ".ziphash" 15 | expectedModuleInfoFile := filepath.Join(expectedCacheDownloadDir, revision.Version) + ".info" 16 | 17 | res := s.storage.GetCacheDownloadPaths(module, revision) 18 | 19 | s.Equal(expectedCacheDownloadDir, res.CacheDownloadDir) 20 | s.Equal(expectedArchiveFile, res.ArchiveFile) 21 | s.Equal(expectedArchiveHashFile, res.ArchiveHashFile) 22 | s.Equal(expectedModuleInfoFile, res.ModuleInfoFile) 23 | } 24 | -------------------------------------------------------------------------------- /internal/adapters/storage/get_install_dir.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "path/filepath" 5 | ) 6 | 7 | // getInstallDir returns dir to install package 8 | // rootDir + installedDir + module full remote path + module's version 9 | // eg: ~/.EASYP/mod/github.com/google/googleapis/v1.2.3 10 | func (s *Storage) GetInstallDir(moduleName string, version string) string { 11 | version = sanitizePath(version) 12 | return filepath.Join(s.rootDir, installedDir, moduleName, version) 13 | } 14 | -------------------------------------------------------------------------------- /internal/adapters/storage/get_install_dir_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "path" 5 | ) 6 | 7 | func (s *storageSuite) Test_GetInstallDir() { 8 | moduleName := getFakeModule().Name 9 | version := getFakeRevision().Version 10 | 11 | expectedResult := path.Join(s.rootDir, installedDir, moduleName, version) 12 | 13 | res := s.storage.GetInstallDir(moduleName, version) 14 | s.Equal(expectedResult, res) 15 | } 16 | -------------------------------------------------------------------------------- /internal/adapters/storage/get_installed_module_hash.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "golang.org/x/mod/sumdb/dirhash" 8 | 9 | "github.com/easyp-tech/easyp/internal/core/models" 10 | ) 11 | 12 | func (s *Storage) GetInstalledModuleHash(moduleName string, revisionVersion string) (models.ModuleHash, error) { 13 | installedDirPath := s.GetInstallDir(moduleName, revisionVersion) 14 | installedPackageHash, err := dirhash.HashDir(installedDirPath, "", dirhash.DefaultHash) 15 | if err != nil { 16 | if os.IsNotExist(err) { 17 | return "", models.ErrModuleNotInstalled 18 | } 19 | 20 | return "", fmt.Errorf("dirhash.HashDir: %w", err) 21 | } 22 | 23 | return models.ModuleHash(installedPackageHash), nil 24 | } 25 | -------------------------------------------------------------------------------- /internal/adapters/storage/install.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "os" 8 | "strings" 9 | 10 | "github.com/codeclysm/extract/v3" 11 | "golang.org/x/mod/sumdb/dirhash" 12 | 13 | "github.com/easyp-tech/easyp/internal/core/models" 14 | ) 15 | 16 | // Install package from archive 17 | // and calculateds hash of installed package 18 | func (s *Storage) Install( 19 | cacheDownloadPaths models.CacheDownloadPaths, 20 | module models.Module, 21 | revision models.Revision, 22 | moduleConfig models.ModuleConfig, 23 | ) (models.ModuleHash, error) { 24 | slog.Info( 25 | "Install package", 26 | "package", module.Name, 27 | "version", revision.Version, 28 | "commit", revision.CommitHash, 29 | ) 30 | 31 | version := sanitizePath(revision.Version) 32 | installedDirPath := s.GetInstallDir(module.Name, version) 33 | 34 | if err := os.MkdirAll(installedDirPath, dirPerm); err != nil { 35 | return "", fmt.Errorf("os.MkdirAll: %w", err) 36 | } 37 | 38 | fp, err := os.Open(cacheDownloadPaths.ArchiveFile) 39 | if err != nil { 40 | return "", fmt.Errorf("os.Open: %w", err) 41 | } 42 | defer func() { _ = fp.Close() }() 43 | 44 | renamer := getRenamer(moduleConfig) 45 | 46 | slog.Debug("Starting extract", "installedDirPath", installedDirPath) 47 | 48 | if err := extract.Archive(context.TODO(), fp, installedDirPath, renamer); err != nil { 49 | return "", fmt.Errorf("extract.Archive: %w", err) 50 | } 51 | 52 | installedPackageHash, err := dirhash.HashDir(installedDirPath, "", dirhash.DefaultHash) 53 | if err != nil { 54 | return "", fmt.Errorf("dirhash.HashDir: %w", err) 55 | } 56 | 57 | return models.ModuleHash(installedPackageHash), nil 58 | } 59 | 60 | // getRenamer return renamer function to convert result files path 61 | func getRenamer(moduleConfig models.ModuleConfig) func(string) string { 62 | return func(file string) string { 63 | for _, dir := range moduleConfig.Directories { 64 | dir := dir + "/" // add trailing slash 65 | 66 | if strings.HasPrefix(file, dir) { 67 | return strings.TrimPrefix(file, dir) 68 | } 69 | } 70 | return file 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/adapters/storage/install_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/easyp-tech/easyp/internal/core/models" 9 | ) 10 | 11 | func TestGetRenamer(t *testing.T) { 12 | tests := map[string]struct { 13 | moduleConfig models.ModuleConfig 14 | passedFile string 15 | expectedResult string 16 | }{ 17 | "directories are empty": { 18 | moduleConfig: models.ModuleConfig{ 19 | Directories: nil, 20 | }, 21 | passedFile: "proto/file.proto", 22 | expectedResult: "proto/file.proto", 23 | }, 24 | "directories contain one dir": { 25 | moduleConfig: models.ModuleConfig{ 26 | Directories: []string{"proto/protovalidate"}, 27 | }, 28 | passedFile: "proto/protovalidate/buf/validate/validate.proto", 29 | expectedResult: "buf/validate/validate.proto", 30 | }, 31 | "directories contain several dirs": { 32 | moduleConfig: models.ModuleConfig{ 33 | Directories: []string{"proto/protovalidate", "proto/protovalidate-testing"}, 34 | }, 35 | passedFile: "proto/protovalidate/buf/validate/validate.proto", 36 | expectedResult: "buf/validate/validate.proto", 37 | }, 38 | } 39 | 40 | for name, tc := range tests { 41 | name, tc := name, tc 42 | t.Run(name, func(t *testing.T) { 43 | renamer := getRenamer(tc.moduleConfig) 44 | result := renamer(tc.passedFile) 45 | require.Equal(t, tc.expectedResult, result) 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /internal/adapters/storage/is_module_installed.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log/slog" 7 | 8 | "github.com/easyp-tech/easyp/internal/core/models" 9 | ) 10 | 11 | func (s *Storage) IsModuleInstalled(module models.Module) (bool, error) { 12 | lockFileInfo, err := s.lockFile.Read(module.Name) 13 | if err != nil { 14 | if errors.Is(err, models.ErrModuleNotFoundInLockFile) { 15 | return false, nil 16 | } 17 | 18 | return false, fmt.Errorf("c.lockFile.Read: %w", err) 19 | } 20 | 21 | if !isVersionsMatched(module.Version, lockFileInfo.Version) { 22 | return false, nil 23 | } 24 | 25 | moduleHash, err := s.GetInstalledModuleHash(module.Name, lockFileInfo.Version) 26 | if err != nil { 27 | if errors.Is(err, models.ErrModuleNotInstalled) { 28 | return false, nil 29 | } 30 | 31 | return false, fmt.Errorf("c.storage.GetInstalledModuleHash: %w", err) 32 | } 33 | 34 | if moduleHash != lockFileInfo.Hash { 35 | slog.Warn("Hashes are not matched", 36 | "LockFileHash", lockFileInfo.Hash, 37 | "Installed module", moduleHash, 38 | ) 39 | 40 | return false, nil 41 | } 42 | 43 | return true, nil 44 | } 45 | 46 | // isVersionsMatched check if passed versions are matched 47 | // or requested version is omitted -> int this case just use version from lockfile 48 | func isVersionsMatched(requestedVersion models.RequestedVersion, lockFileVersion string) bool { 49 | return requestedVersion.IsOmitted() || string(requestedVersion) == lockFileVersion 50 | } 51 | -------------------------------------------------------------------------------- /internal/adapters/storage/is_module_installed_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/brianvoe/gofakeit/v6" 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/easyp-tech/easyp/internal/core/models" 10 | ) 11 | 12 | func Test_isVersionsMatched(t *testing.T) { 13 | tests := map[string]struct { 14 | requestedVersion models.RequestedVersion 15 | lockFileVersion string 16 | expectedResult bool 17 | }{ 18 | "requested version is omitted": { 19 | requestedVersion: models.Omitted, 20 | lockFileVersion: gofakeit.Word(), 21 | expectedResult: true, 22 | }, 23 | "requested version and lock file are matched": { 24 | requestedVersion: "v1.2.3", 25 | lockFileVersion: "v1.2.3", 26 | expectedResult: true, 27 | }, 28 | "requested version and lock file are not matched": { 29 | requestedVersion: "v1.2.3-1", 30 | lockFileVersion: "v1.2.3", 31 | expectedResult: false, 32 | }, 33 | } 34 | 35 | for name, tc := range tests { 36 | name, tc := name, tc 37 | t.Run(name, func(t *testing.T) { 38 | result := isVersionsMatched(tc.requestedVersion, tc.lockFileVersion) 39 | require.Equal(t, tc.expectedResult, result) 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/adapters/storage/sanitize.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import "strings" 4 | 5 | func sanitizePath(source string) string { 6 | return strings.ReplaceAll(source, "/", "-") 7 | } 8 | -------------------------------------------------------------------------------- /internal/adapters/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core/models" 5 | ) 6 | 7 | const ( 8 | // root cache dir 9 | cacheDir = "cache" 10 | // dir for downloaded (check sum, archive) 11 | cacheDownloadDir = "download" 12 | // dir for installed packages 13 | installedDir = "mod" 14 | ) 15 | 16 | type ( 17 | // LockFile should implement adapter for lock file workflow 18 | LockFile interface { 19 | Read(moduleName string) (models.LockFileInfo, error) 20 | } 21 | 22 | // Storage implements workflows with directories 23 | Storage struct { 24 | rootDir string 25 | lockFile LockFile 26 | } 27 | ) 28 | 29 | func New(rootDir string, lockFile LockFile) *Storage { 30 | return &Storage{ 31 | rootDir: rootDir, 32 | lockFile: lockFile, 33 | } 34 | } 35 | 36 | const ( 37 | dirPerm = 0755 38 | ) 39 | -------------------------------------------------------------------------------- /internal/adapters/storage/storage_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "path" 5 | "testing" 6 | 7 | "github.com/brianvoe/gofakeit/v6" 8 | "github.com/stretchr/testify/suite" 9 | 10 | "github.com/easyp-tech/easyp/internal/adapters/storage/mocks" 11 | "github.com/easyp-tech/easyp/internal/core/models" 12 | ) 13 | 14 | type storageSuite struct { 15 | suite.Suite 16 | 17 | rootDir string 18 | lockFile *mocks.LockFile 19 | storage *Storage 20 | } 21 | 22 | func getFakeModule() models.Module { 23 | name := path.Join(gofakeit.DomainName(), gofakeit.Word(), gofakeit.Word()) 24 | 25 | return models.Module{ 26 | Name: name, 27 | Version: models.RequestedVersion(gofakeit.Word()), 28 | } 29 | } 30 | 31 | func getFakeRevision() models.Revision { 32 | return models.Revision{ 33 | CommitHash: gofakeit.UUID(), 34 | Version: gofakeit.Word(), 35 | } 36 | } 37 | 38 | func (s *storageSuite) SetupTest() { 39 | s.rootDir = "/" + path.Join(gofakeit.Word(), gofakeit.Word()) 40 | s.lockFile = mocks.NewLockFile(s.T()) 41 | 42 | s.storage = New(s.rootDir, s.lockFile) 43 | } 44 | 45 | func TestRunSuite(t *testing.T) { 46 | suite.Run(t, new(storageSuite)) 47 | } 48 | -------------------------------------------------------------------------------- /internal/api/enum.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type EnumValue struct { 9 | Enum []string 10 | Default string 11 | selected string 12 | } 13 | 14 | func (e *EnumValue) Set(value string) error { 15 | for _, enum := range e.Enum { 16 | if enum == value { 17 | e.selected = value 18 | return nil 19 | } 20 | } 21 | 22 | return fmt.Errorf("allowed values are %s", strings.Join(e.Enum, ", ")) 23 | } 24 | 25 | func (e EnumValue) String() string { 26 | if e.selected == "" { 27 | return e.Default 28 | } 29 | return e.selected 30 | } 31 | -------------------------------------------------------------------------------- /internal/api/generate.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/urfave/cli/v2" 8 | 9 | "github.com/easyp-tech/easyp/internal/config" 10 | "github.com/easyp-tech/easyp/internal/flags" 11 | "github.com/easyp-tech/easyp/internal/fs/fs" 12 | ) 13 | 14 | var _ Handler = (*Generate)(nil) 15 | 16 | // Generate is a handler for generate command. 17 | type Generate struct{} 18 | 19 | var ( 20 | flagGenerateDirectoryPath = &cli.StringFlag{ 21 | Name: "path", 22 | Usage: "set path to directory with proto files", 23 | Required: true, 24 | HasBeenSet: true, 25 | Value: ".", 26 | Aliases: []string{"p"}, 27 | EnvVars: []string{"EASYP_ROOT_GENERATE_PATH"}, 28 | } 29 | ) 30 | 31 | // Command implements Handler. 32 | func (g Generate) Command() *cli.Command { 33 | return &cli.Command{ 34 | Name: "generate", 35 | Aliases: []string{"g"}, 36 | Usage: "generate code from proto files", 37 | UsageText: "generate code from proto files", 38 | Description: "generate code from proto files", 39 | Action: g.Action, 40 | Flags: []cli.Flag{ 41 | flagGenerateDirectoryPath, 42 | }, 43 | HelpName: "help", 44 | } 45 | } 46 | 47 | // Action implements Handler. 48 | func (g Generate) Action(ctx *cli.Context) error { 49 | workingDir, err := os.Getwd() 50 | if err != nil { 51 | return fmt.Errorf("os.Getwd: %w", err) 52 | } 53 | 54 | cfg, err := config.New(ctx.Context, ctx.String(flags.Config.Name)) 55 | if err != nil { 56 | return fmt.Errorf("config.New: %w", err) 57 | } 58 | dirWalker := fs.NewFSWalker(workingDir, ".") 59 | app, err := buildCore(ctx.Context, *cfg, dirWalker) 60 | if err != nil { 61 | return fmt.Errorf("buildCore: %w", err) 62 | } 63 | 64 | dir := ctx.String(flagGenerateDirectoryPath.Name) 65 | err = app.Generate(ctx.Context, ".", dir) 66 | if err != nil { 67 | return fmt.Errorf("generator.Generate: %w", err) 68 | } 69 | 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /internal/api/init.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/urfave/cli/v2" 7 | 8 | "github.com/easyp-tech/easyp/internal/config" 9 | "github.com/easyp-tech/easyp/internal/fs/fs" 10 | ) 11 | 12 | var _ Handler = (*Init)(nil) 13 | 14 | // Init is a handler for initialization EasyP configuration. 15 | type Init struct{} 16 | 17 | var ( 18 | flagInitDirectoryPath = &cli.StringFlag{ 19 | Name: "dir", 20 | Usage: "directory path to initialize", 21 | Required: true, 22 | HasBeenSet: true, 23 | Value: ".", 24 | Aliases: []string{"d"}, 25 | EnvVars: []string{"EASYP_INIT_DIR"}, 26 | } 27 | ) 28 | 29 | // Command implements Handler. 30 | func (i Init) Command() *cli.Command { 31 | return &cli.Command{ 32 | Name: "init", 33 | Aliases: []string{"i"}, 34 | Usage: "initialize configuration", 35 | UsageText: "initialize configuration", 36 | Description: "initialize configuration", 37 | Action: i.Action, 38 | Flags: []cli.Flag{ 39 | flagInitDirectoryPath, 40 | }, 41 | } 42 | } 43 | 44 | // Action implements Handler. 45 | func (i Init) Action(ctx *cli.Context) error { 46 | rootPath := ctx.String(flagInitDirectoryPath.Name) 47 | dirFS := fs.NewFSWalker(rootPath, ".") 48 | 49 | cfg := &config.Config{} 50 | 51 | app, err := buildCore(ctx.Context, *cfg, dirFS) 52 | if err != nil { 53 | return fmt.Errorf("buildCore: %w", err) 54 | } 55 | 56 | err = app.Initialize(ctx.Context, dirFS, []string{"DEFAULT"}) 57 | if err != nil { 58 | return fmt.Errorf("initer.Initialize: %w", err) 59 | } 60 | 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /internal/api/interface.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | ) 6 | 7 | // Handler is an interface for a handling command. 8 | type Handler interface { 9 | // Command returns a command. 10 | Command() *cli.Command 11 | } 12 | -------------------------------------------------------------------------------- /internal/config/breaking_check.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // BreakingCheck is the configuration for `breaking` command 4 | type BreakingCheck struct { 5 | Ignore []string `json:"ignore" yaml:"ignore"` 6 | // git ref to compare with 7 | AgainstGitRef string `json:"against_git_ref" yaml:"against_git_ref"` 8 | } 9 | -------------------------------------------------------------------------------- /internal/config/default.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Default configurations. 4 | const ( 5 | DefaultFileName = "easyp.yaml" 6 | ) 7 | -------------------------------------------------------------------------------- /internal/config/lint.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // LintConfig contains linter configuration. 4 | type LintConfig struct { 5 | Use []string `json:"use" yaml:"use" env:"USE"` // Use rules for linter. 6 | EnumZeroValueSuffix string `json:"enum_zero_value_suffix" yaml:"enum_zero_value_suffix" env:"ENUM_ZERO_VALUE_SUFFIX"` // Enum zero value suffix. 7 | ServiceSuffix string `json:"service_suffix" yaml:"service_suffix" env:"SERVICE_SUFFIX"` // Service suffix. 8 | Ignore []string `json:"ignore" yaml:"ignore" env:"IGNORE"` // Ignore dirs with proto file. 9 | Except []string `json:"except" yaml:"except" env:"EXCEPT"` // Except linter rules. 10 | AllowCommentIgnores bool `json:"allow_comment_ignores" yaml:"allow_comment_ignores" env:"ALLOW_COMMENT_IGNORES"` // Allow comment ignore. 11 | IgnoreOnly map[string][]string `json:"ignore_only" yaml:"ignore_only" env:"IGNORE_ONLY"` 12 | } 13 | -------------------------------------------------------------------------------- /internal/core/check_lint_ignore.go: -------------------------------------------------------------------------------- 1 | package core 2 | -------------------------------------------------------------------------------- /internal/core/core.go: -------------------------------------------------------------------------------- 1 | // Package core contains every logic for working cli. 2 | package core 3 | 4 | import ( 5 | "errors" 6 | "log/slog" 7 | ) 8 | 9 | // Core provide to business logic of EasyP. 10 | type Core struct { 11 | rules []Rule 12 | ignore []string 13 | deps []string 14 | ignoreOnly map[string][]string 15 | logger *slog.Logger 16 | plugins []Plugin 17 | inputs Inputs 18 | console Console 19 | storage Storage 20 | moduleConfig ModuleConfig 21 | lockFile LockFile 22 | 23 | breakingCheckConfig BreakingCheckConfig 24 | currentProjectGitWalker CurrentProjectGitWalker 25 | } 26 | 27 | var ( 28 | ErrInvalidRule = errors.New("invalid rule") 29 | ErrRepositoryDoesNotExist = errors.New("repository does not exist") 30 | ) 31 | 32 | func New( 33 | rules []Rule, 34 | ignore []string, 35 | deps []string, 36 | ignoreOnly map[string][]string, 37 | logger *slog.Logger, 38 | plugins []Plugin, 39 | inputs Inputs, 40 | console Console, 41 | storage Storage, 42 | moduleConfig ModuleConfig, 43 | lockFile LockFile, 44 | currentProjectGitWalker CurrentProjectGitWalker, 45 | breakingCheckConfig BreakingCheckConfig, 46 | ) *Core { 47 | return &Core{ 48 | rules: rules, 49 | ignore: ignore, 50 | deps: deps, 51 | ignoreOnly: ignoreOnly, 52 | logger: logger, 53 | plugins: plugins, 54 | inputs: inputs, 55 | console: console, 56 | storage: storage, 57 | moduleConfig: moduleConfig, 58 | lockFile: lockFile, 59 | currentProjectGitWalker: currentProjectGitWalker, 60 | breakingCheckConfig: breakingCheckConfig, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /internal/core/download.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log/slog" 8 | 9 | "github.com/easyp-tech/easyp/internal/core/models" 10 | ) 11 | 12 | // Download all packages from config 13 | // dependencies slice of strings format: origin@version: github.com/company/repository@v1.2.3 14 | // if version is absent use the latest commit 15 | func (c *Core) Download(ctx context.Context, dependencies []string) error { 16 | if c.lockFile.IsEmpty() { 17 | // if lock file is empty or doesn't exist install versions 18 | // from easyp.yaml config and create lock file 19 | slog.Debug("Lock file is empty") 20 | return c.Update(ctx, dependencies) 21 | } 22 | 23 | slog.Debug("Lock file is not empty. Install deps from it") 24 | 25 | for lockFileInfo := range c.lockFile.DepsIter() { 26 | module := models.NewModuleFromLockFileInfo(lockFileInfo) 27 | 28 | isInstalled, err := c.storage.IsModuleInstalled(module) 29 | if err != nil { 30 | return fmt.Errorf("c.isModuleInstalled: %w", err) 31 | } 32 | 33 | if isInstalled { 34 | slog.Info("Module is installed", "name", module.Name, "version", module.Version) 35 | continue 36 | } 37 | 38 | if err := c.Get(ctx, module); err != nil { 39 | if errors.Is(err, models.ErrVersionNotFound) { 40 | slog.Error("Version not found", "name", module.Name, "version", module.Version) 41 | return models.ErrVersionNotFound 42 | } 43 | 44 | return fmt.Errorf("c.Get: %w", err) 45 | } 46 | } 47 | 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/core/fs.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type DirWalker interface { 8 | FS 9 | WalkDir(callback func(path string, err error) error) error 10 | } 11 | 12 | // FS an interface for reading from some FS (os disk, git repo etc) 13 | // and for writing to some FS 14 | type FS interface { 15 | Open(name string) (io.ReadCloser, error) 16 | Create(name string) (io.WriteCloser, error) 17 | } 18 | -------------------------------------------------------------------------------- /internal/core/instruction_parser.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // instructionInfo collects info about instruction in proto file 9 | // e.g `google.api.http`: 10 | // 11 | // `google.api` - package name 12 | // 'http' - instruction name 13 | type InstructionInfo struct { 14 | PkgName PackageName 15 | Instruction string 16 | } 17 | 18 | func (i InstructionInfo) GetFullName() string { 19 | return fmt.Sprintf("%s.%s", i.PkgName, i.Instruction) 20 | } 21 | 22 | // parseInstruction parse input string and return its package name 23 | // if passed input does not have package -> return pkgName as package name source proto file 24 | type InstructionParser struct { 25 | SourcePkgName PackageName 26 | } 27 | 28 | func (p InstructionParser) Parse(input string) InstructionInfo { 29 | // check if there is brackets, and extract 30 | // (google.api.http) -> google.api.http 31 | // (buf.validate.field).string.uuid -> buf.validate.field 32 | // or pkg.FieldType -> pkg.FieldType 33 | iStart := strings.Index(input, "(") 34 | iEnd := strings.Index(input, ")") 35 | if iStart != -1 && iEnd != -1 { 36 | input = input[iStart+1 : iEnd] 37 | } 38 | 39 | idx := strings.LastIndex(input, ".") 40 | if idx <= 0 { 41 | return InstructionInfo{ 42 | PkgName: p.SourcePkgName, 43 | Instruction: input, 44 | } 45 | } 46 | 47 | return InstructionInfo{ 48 | PkgName: PackageName(input[:idx]), 49 | Instruction: input[idx+1:], 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /internal/core/instruction_parser_test.go: -------------------------------------------------------------------------------- 1 | package core_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/easyp-tech/easyp/internal/core" 9 | ) 10 | 11 | func Test_InstructionParser_Parse(t *testing.T) { 12 | tests := map[string]struct { 13 | sourcePkgName core.PackageName 14 | source string 15 | 16 | pkgName core.PackageName 17 | instructionName string 18 | }{ 19 | "google_api_package": { 20 | sourcePkgName: "google/api", 21 | source: "(google.api.http)", 22 | pkgName: "google.api", 23 | instructionName: "http", 24 | }, 25 | "mine_package": { 26 | sourcePkgName: "mine", 27 | source: "SomeMessage", 28 | pkgName: "mine", 29 | instructionName: "SomeMessage", 30 | }, 31 | } 32 | 33 | for name, test := range tests { 34 | t.Run(name, func(t *testing.T) { 35 | parser := core.InstructionParser{SourcePkgName: test.sourcePkgName} 36 | 37 | res := parser.Parse(test.source) 38 | require.Equal(t, test.pkgName, res.PkgName) 39 | require.Equal(t, test.instructionName, res.Instruction) 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/core/mod.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "context" 5 | "iter" 6 | 7 | "github.com/easyp-tech/easyp/internal/adapters/repository" 8 | 9 | "github.com/easyp-tech/easyp/internal/core/models" 10 | ) 11 | 12 | type ( 13 | // Storage should implement workflow with storage adapter 14 | Storage interface { 15 | CreateCacheRepositoryDir(name string) (string, error) 16 | CreateCacheDownloadDir(models.CacheDownloadPaths) error 17 | GetCacheDownloadPaths(module models.Module, revision models.Revision) models.CacheDownloadPaths 18 | Install( 19 | cacheDownloadPaths models.CacheDownloadPaths, 20 | module models.Module, 21 | revision models.Revision, 22 | moduleConfig models.ModuleConfig, 23 | ) (models.ModuleHash, error) 24 | GetInstalledModuleHash(moduleName string, revisionVersion string) (models.ModuleHash, error) 25 | IsModuleInstalled(module models.Module) (bool, error) 26 | GetInstallDir(moduleName string, revisionVersion string) string 27 | } 28 | 29 | // ModuleConfig should implement adapter for reading module configs 30 | ModuleConfig interface { 31 | ReadFromRepo(ctx context.Context, repo repository.Repo, revision models.Revision) (models.ModuleConfig, error) 32 | } 33 | 34 | // LockFile should implement adapter for lock file workflow 35 | LockFile interface { 36 | Read(moduleName string) (models.LockFileInfo, error) 37 | Write( 38 | moduleName string, revisionVersion string, installedPackageHash models.ModuleHash, 39 | ) error 40 | IsEmpty() bool 41 | DepsIter() iter.Seq[models.LockFileInfo] 42 | } 43 | 44 | // Mod implement package manager's commands 45 | //Mod struct { 46 | // storage Storage 47 | // moduleConfig ModuleConfig 48 | // lockFile LockFile 49 | //} 50 | ) 51 | 52 | //func New(storage Storage, moduleConfig ModuleConfig, lockFile LockFile) *Mod { 53 | // return &Mod{ 54 | // storage: storage, 55 | // moduleConfig: moduleConfig, 56 | // lockFile: lockFile, 57 | // } 58 | //} 59 | -------------------------------------------------------------------------------- /internal/core/models/cache_download_paths.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // CacheDownloadPaths collects cache download paths to: 4 | // * archive 5 | // * file with archive hash 6 | // * info about downloaded module 7 | type CacheDownloadPaths struct { 8 | // CacheDownload path to dir with downloaded cache 9 | CacheDownloadDir string 10 | 11 | // ArchiveFile full path to downloaded archive of module 12 | ArchiveFile string 13 | 14 | // ArchiveHashFile full path to file with hash of archive 15 | ArchiveHashFile string 16 | 17 | // ModuleInfoFile full path to file with info about downloaded module 18 | ModuleInfoFile string 19 | } 20 | -------------------------------------------------------------------------------- /internal/core/models/errors.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | ErrVersionNotFound = errors.New("version not found") 9 | ErrFileNotFound = errors.New("file not found") 10 | ErrModuleNotInstalled = errors.New("module not installed") 11 | ) 12 | -------------------------------------------------------------------------------- /internal/core/models/lock_file_info.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // LockFileInfo contains information about module from lock file 8 | type LockFileInfo struct { 9 | Name string 10 | Version string 11 | Hash ModuleHash 12 | } 13 | 14 | var ( 15 | ErrModuleNotFoundInLockFile = errors.New("module not found in lock file") 16 | ) 17 | -------------------------------------------------------------------------------- /internal/core/models/module_config.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // ModuleConfig contains module config such as dirs from buf.work.yaml 4 | type ModuleConfig struct { 5 | // Directories contains dirs with proto files (buf.work.yaml) 6 | Directories []string 7 | 8 | // Dependencies contains list of required dependencies in repository 9 | // it could be from easyp.yaml or from buf 10 | Dependencies []Module 11 | } 12 | -------------------------------------------------------------------------------- /internal/core/models/revision.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Revision collects references to module's commit 4 | // Revision is actual module information from repository 5 | type Revision struct { 6 | CommitHash string // commit's hash 7 | Version string // commit's tag or generated version 8 | } 9 | -------------------------------------------------------------------------------- /internal/core/path_helpers/is_target_path.go: -------------------------------------------------------------------------------- 1 | package path_helpers 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | // IsTargetPath check if passed filePath is target 10 | // it has to be in targetPath dir 11 | func IsTargetPath(targetPath, filePath string) bool { 12 | rel, err := filepath.Rel(targetPath, filePath) 13 | if err != nil { 14 | return false 15 | } 16 | if !filepath.IsLocal(rel) { 17 | return false 18 | } 19 | 20 | return true 21 | } 22 | 23 | func IsIgnoredPath(path string, ignore []string) bool { 24 | up := ".." + string(os.PathSeparator) 25 | 26 | for _, ignorePath := range ignore { 27 | rel, err := filepath.Rel(ignorePath, path) 28 | if err != nil { 29 | continue 30 | } 31 | if strings.HasPrefix(rel, up) && rel != ".." { 32 | continue 33 | } 34 | return true 35 | } 36 | 37 | return false 38 | } 39 | -------------------------------------------------------------------------------- /internal/core/update.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log/slog" 8 | 9 | "github.com/easyp-tech/easyp/internal/core/models" 10 | ) 11 | 12 | // Update all packages from config 13 | // dependencies slice of strings format: origin@version: github.com/company/repository@v1.2.3 14 | // if version is absent use the latest commit 15 | func (c *Core) Update(ctx context.Context, dependencies []string) error { 16 | for _, dependency := range dependencies { 17 | 18 | module := models.NewModule(dependency) 19 | 20 | isInstalled, err := c.storage.IsModuleInstalled(module) 21 | if err != nil { 22 | return fmt.Errorf("c.isModuleInstalled: %w", err) 23 | } 24 | 25 | if isInstalled { 26 | slog.Info("Module is installed", "name", module.Name, "version", module.Version) 27 | continue 28 | } 29 | 30 | if err := c.Get(ctx, module); err != nil { 31 | if errors.Is(err, models.ErrVersionNotFound) { 32 | slog.Error("Version not found", "dependency", dependency) 33 | return models.ErrVersionNotFound 34 | } 35 | 36 | return fmt.Errorf("c.Get: %w", err) 37 | } 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/core/vendor.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | cp "github.com/otiai10/copy" 8 | ) 9 | 10 | const ( 11 | // TODO: move to config 12 | easypVendorDir = "easyp_vendor" 13 | ) 14 | 15 | // Vendor copy all proto files from deps to local dir 16 | func (c *Core) Vendor(ctx context.Context) error { 17 | if err := c.Download(ctx, c.deps); err != nil { 18 | return fmt.Errorf("c.Download: %w", err) 19 | } 20 | 21 | for dep := range c.lockFile.DepsIter() { 22 | depPath, err := c.getModulePath(ctx, dep.Name) 23 | if err != nil { 24 | return fmt.Errorf("c.moduleReflect.GetModulePath: %w", err) 25 | } 26 | 27 | if err := cp.Copy(depPath, easypVendorDir); err != nil { 28 | return fmt.Errorf("c.Copy: %w", err) 29 | } 30 | } 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /internal/flags/flags.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | ) 6 | 7 | const ( 8 | globalCategory = "global" 9 | ) 10 | 11 | const ( 12 | // Max file size is 1mb. 13 | defaultConfigFilePath = "easyp.yaml" 14 | ) 15 | 16 | // Flags. 17 | var ( 18 | Config = &cli.StringFlag{ 19 | Name: "cfg", 20 | Category: globalCategory, 21 | DefaultText: "specify the path to the configuration file", 22 | FilePath: "", 23 | Usage: "Specify the absolute or relative path to the configuration file for setting up the application.", 24 | Required: true, 25 | Hidden: false, 26 | HasBeenSet: true, 27 | Value: defaultConfigFilePath, 28 | Aliases: []string{"config"}, 29 | EnvVars: []string{"EASYP_CFG"}, 30 | TakesFile: true, 31 | } 32 | 33 | DebugMode = &cli.BoolFlag{ 34 | Name: "debug", 35 | Usage: "Enable debug mode to get more detailed information in logs.", 36 | Required: false, 37 | Value: false, 38 | Aliases: []string{"d"}, 39 | EnvVars: []string{"EASYP_DEBUG"}, 40 | } 41 | ) 42 | -------------------------------------------------------------------------------- /internal/fs/fs/adapter.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "io" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | type FSAdapter struct { 11 | fs.FS 12 | 13 | rootDir string 14 | } 15 | 16 | func (a *FSAdapter) Open(name string) (io.ReadCloser, error) { 17 | return a.FS.Open(name) 18 | } 19 | 20 | func (a *FSAdapter) Create(name string) (io.WriteCloser, error) { 21 | path := filepath.Join(a.rootDir, name) 22 | return os.Create(path) 23 | } 24 | -------------------------------------------------------------------------------- /internal/fs/fs/dir_walker.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "io" 5 | "io/fs" 6 | "os" 7 | ) 8 | 9 | type FS interface { 10 | Open(name string) (io.ReadCloser, error) 11 | Create(name string) (io.WriteCloser, error) 12 | } 13 | 14 | func NewFSWalker(root, path string) *FSWalker { 15 | if path == "" { 16 | path = "." 17 | } 18 | 19 | diskFS := os.DirFS(root) 20 | return &FSWalker{ 21 | FSAdapter: &FSAdapter{diskFS, root}, 22 | path: path, 23 | } 24 | } 25 | 26 | type FSWalker struct { 27 | *FSAdapter 28 | 29 | path string 30 | } 31 | 32 | func (w *FSWalker) WalkDir(callback func(path string, err error) error) error { 33 | err := fs.WalkDir(w.FS, w.path, func(path string, d fs.DirEntry, err error) error { 34 | return callback(path, err) 35 | }) 36 | 37 | return err 38 | } 39 | -------------------------------------------------------------------------------- /internal/fs/go_git/adapter.go: -------------------------------------------------------------------------------- 1 | package go_git 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | 7 | "github.com/go-git/go-git/v5/plumbing/object" 8 | ) 9 | 10 | type GitTreeDiskAdapter struct { 11 | *object.Tree 12 | } 13 | 14 | func (a *GitTreeDiskAdapter) Open(name string) (io.ReadCloser, error) { 15 | gitFile, err := a.File(name) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return gitFile.Reader() 21 | } 22 | 23 | func (a *GitTreeDiskAdapter) Create(name string) (io.WriteCloser, error) { 24 | return nil, errors.New("not implemented") 25 | } 26 | -------------------------------------------------------------------------------- /internal/fs/go_git/dir_walker.go: -------------------------------------------------------------------------------- 1 | package go_git 2 | 3 | import ( 4 | "github.com/go-git/go-git/v5/plumbing/object" 5 | 6 | "github.com/easyp-tech/easyp/internal/core/path_helpers" 7 | ) 8 | 9 | func NewGitTreeWalker(tree *object.Tree, path string) *GitTreeWalker { 10 | return &GitTreeWalker{ 11 | GitTreeDiskAdapter: &GitTreeDiskAdapter{tree}, 12 | tree: tree, 13 | path: path, 14 | } 15 | } 16 | 17 | type GitTreeWalker struct { 18 | *GitTreeDiskAdapter 19 | 20 | tree *object.Tree 21 | path string 22 | } 23 | 24 | func (w *GitTreeWalker) WalkDir(callback func(path string, err error) error) error { 25 | err := w.tree.Files().ForEach(func(f *object.File) error { 26 | switch { 27 | case !path_helpers.IsTargetPath(w.path, f.Name): 28 | return nil 29 | } 30 | 31 | return callback(f.Name, nil) 32 | }) 33 | return err 34 | } 35 | -------------------------------------------------------------------------------- /internal/rules/comment_enum.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | var _ core.Rule = (*CommentEnum)(nil) 8 | 9 | // CommentEnum this rule checks that enums have non-empty comments. 10 | type CommentEnum struct{} 11 | 12 | // Message implements lint.Rule. 13 | func (c *CommentEnum) Message() string { 14 | return "enum comments must not be empty" 15 | } 16 | 17 | // Validate implements lint.Rule. 18 | func (c *CommentEnum) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 19 | var res []core.Issue 20 | 21 | for _, enum := range protoInfo.Info.ProtoBody.Enums { 22 | if len(enum.Comments) == 0 { 23 | res = core.AppendIssue(res, c, enum.Meta.Pos, enum.EnumName, enum.Comments) 24 | } 25 | } 26 | 27 | for _, msg := range protoInfo.Info.ProtoBody.Messages { 28 | for _, enum := range msg.MessageBody.Enums { 29 | if len(enum.Comments) == 0 { 30 | res = core.AppendIssue(res, c, enum.Meta.Pos, enum.EnumName, enum.Comments) 31 | } 32 | } 33 | } 34 | 35 | return res, nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/rules/comment_enum_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestCommentEnum_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "enum comments must not be empty" 19 | 20 | rule := rules.CommentEnum{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestCommentEnum_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Offset: 864, 39 | Line: 49, 40 | Column: 1, 41 | }, 42 | SourceName: "social_network", 43 | Message: "enum comments must not be empty", 44 | RuleName: "COMMENT_ENUM", 45 | }, 46 | wantErr: nil, 47 | }, 48 | "invalid_nested": { 49 | fileName: invalidAuthProto, 50 | wantIssues: &core.Issue{ 51 | Position: meta.Position{ 52 | Filename: "", 53 | Offset: 610, 54 | Line: 31, 55 | Column: 3, 56 | }, 57 | SourceName: "social_network", 58 | Message: "enum comments must not be empty", 59 | RuleName: "COMMENT_ENUM", 60 | }, 61 | wantErr: nil, 62 | }, 63 | "valid": { 64 | fileName: validAuthProto, 65 | wantErr: nil, 66 | }, 67 | } 68 | 69 | for name, tc := range tests { 70 | name, tc := name, tc 71 | t.Run(name, func(t *testing.T) { 72 | t.Parallel() 73 | 74 | r, protos := start(t) 75 | 76 | rule := rules.CommentEnum{} 77 | 78 | issues, err := rule.Validate(protos[tc.fileName]) 79 | r.ErrorIs(err, tc.wantErr) 80 | switch { 81 | case tc.wantIssues != nil: 82 | r.Contains(issues, *tc.wantIssues) 83 | case len(issues) > 0: 84 | r.Empty(issues) 85 | } 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/rules/comment_enum_value.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | var _ core.Rule = (*CommentEnumValue)(nil) 8 | 9 | // CommentEnumValue this rule checks that enum values have non-empty comments. 10 | type CommentEnumValue struct{} 11 | 12 | // Message implements lint.Rule. 13 | func (c *CommentEnumValue) Message() string { 14 | return "enum value comments must not be empty" 15 | } 16 | 17 | // Validate implements lint.Rule. 18 | func (c *CommentEnumValue) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 19 | var res []core.Issue 20 | 21 | for _, enum := range protoInfo.Info.ProtoBody.Enums { 22 | for _, field := range enum.EnumBody.EnumFields { 23 | if len(field.Comments) == 0 { 24 | res = core.AppendIssue( 25 | res, 26 | c, 27 | field.Meta.Pos, 28 | field.Ident, 29 | field.Comments) 30 | } 31 | } 32 | } 33 | 34 | for _, msg := range protoInfo.Info.ProtoBody.Messages { 35 | for _, enum := range msg.MessageBody.Enums { 36 | for _, field := range enum.EnumBody.EnumFields { 37 | if len(field.Comments) == 0 { 38 | res = core.AppendIssue( 39 | res, 40 | c, 41 | field.Meta.Pos, 42 | field.Ident, 43 | field.Comments) 44 | } 45 | } 46 | } 47 | } 48 | 49 | return res, nil 50 | } 51 | -------------------------------------------------------------------------------- /internal/rules/comment_enum_value_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestCommentEnumValue_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "enum value comments must not be empty" 19 | 20 | rule := rules.CommentEnumValue{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestCommentEnumValue_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 917, 40 | Line: 51, 41 | Column: 3, 42 | }, 43 | SourceName: "none", 44 | Message: "enum value comments must not be empty", 45 | RuleName: "COMMENT_ENUM_VALUE", 46 | }, 47 | wantErr: nil, 48 | }, 49 | "invalid_nested": { 50 | fileName: invalidAuthProto, 51 | wantIssues: &core.Issue{ 52 | Position: meta.Position{ 53 | Filename: "", 54 | Offset: 667, 55 | Line: 33, 56 | Column: 5, 57 | }, 58 | SourceName: "none", 59 | Message: "enum value comments must not be empty", 60 | RuleName: "COMMENT_ENUM_VALUE", 61 | }, 62 | wantErr: nil, 63 | }, 64 | "valid": { 65 | fileName: validAuthProto, 66 | wantErr: nil, 67 | }, 68 | } 69 | 70 | for name, tc := range tests { 71 | name, tc := name, tc 72 | t.Run(name, func(t *testing.T) { 73 | t.Parallel() 74 | 75 | r, protos := start(t) 76 | 77 | rule := rules.CommentEnumValue{} 78 | issues, err := rule.Validate(protos[tc.fileName]) 79 | r.ErrorIs(err, tc.wantErr) 80 | switch { 81 | case tc.wantIssues != nil: 82 | r.Contains(issues, *tc.wantIssues) 83 | case len(issues) > 0: 84 | r.Empty(issues) 85 | } 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/rules/comment_field_message.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | var _ core.Rule = (*CommentField)(nil) 8 | 9 | // CommentField this rule checks that fields have non-empty comments. 10 | type CommentField struct{} 11 | 12 | // Message implements lint.Rule. 13 | func (c *CommentField) Message() string { 14 | return "field comments must not be empty" 15 | } 16 | 17 | // Validate implements lint.Rule. 18 | func (c *CommentField) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 19 | var res []core.Issue 20 | 21 | for _, message := range protoInfo.Info.ProtoBody.Messages { 22 | for _, field := range message.MessageBody.Fields { 23 | if len(field.Comments) == 0 { 24 | res = core.AppendIssue(res, c, field.Meta.Pos, field.FieldName, field.Comments) 25 | } 26 | } 27 | } 28 | 29 | return res, nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/rules/comment_field_message_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestCommentField_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "field comments must not be empty" 19 | 20 | rule := rules.CommentField{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | 25 | } 26 | 27 | func TestCommentField_Validate(t *testing.T) { 28 | t.Parallel() 29 | 30 | tests := map[string]struct { 31 | fileName string 32 | wantIssues *core.Issue 33 | wantErr error 34 | }{ 35 | "invalid": { 36 | fileName: invalidAuthProto, 37 | wantIssues: &core.Issue{ 38 | Position: meta.Position{ 39 | Filename: "", 40 | Offset: 447, 41 | Line: 18, 42 | Column: 3, 43 | }, 44 | SourceName: "token", 45 | Message: "field comments must not be empty", 46 | RuleName: "COMMENT_FIELD", 47 | }, 48 | wantErr: nil, 49 | }, 50 | "valid": { 51 | fileName: validAuthProto, 52 | wantErr: nil, 53 | }, 54 | } 55 | 56 | for name, tc := range tests { 57 | name, tc := name, tc 58 | t.Run(name, func(t *testing.T) { 59 | t.Parallel() 60 | 61 | r, protos := start(t) 62 | 63 | rule := rules.CommentField{} 64 | issues, err := rule.Validate(protos[tc.fileName]) 65 | r.ErrorIs(err, tc.wantErr) 66 | switch { 67 | case tc.wantIssues != nil: 68 | r.Contains(issues, *tc.wantIssues) 69 | case len(issues) > 0: 70 | r.Empty(issues) 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /internal/rules/comment_message.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | var _ core.Rule = (*CommentMessage)(nil) 8 | 9 | // CommentMessage this rule checks that messages have non-empty comments. 10 | type CommentMessage struct{} 11 | 12 | // Message implements lint.Rule. 13 | func (c *CommentMessage) Message() string { 14 | return "message comment is empty" 15 | } 16 | 17 | // Validate implements lint.Rule. 18 | func (c *CommentMessage) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 19 | var res []core.Issue 20 | 21 | for _, message := range protoInfo.Info.ProtoBody.Messages { 22 | if len(message.Comments) == 0 { 23 | res = core.AppendIssue(res, c, message.Meta.Pos, message.MessageName, message.Comments) 24 | } 25 | } 26 | 27 | return res, nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/rules/comment_message_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestCommentMessage_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "message comment is empty" 19 | 20 | rule := rules.CommentMessage{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | 25 | } 26 | 27 | func TestCommentMessage_Validate(t *testing.T) { 28 | t.Parallel() 29 | 30 | tests := map[string]struct { 31 | fileName string 32 | wantIssues *core.Issue 33 | wantErr error 34 | }{ 35 | "invalid": { 36 | fileName: invalidAuthProto, 37 | wantIssues: &core.Issue{ 38 | Position: meta.Position{ 39 | Filename: "", 40 | Offset: 425, 41 | Line: 17, 42 | Column: 1, 43 | }, 44 | SourceName: "TokenData", 45 | Message: "message comment is empty", 46 | RuleName: "COMMENT_MESSAGE", 47 | }, 48 | wantErr: nil, 49 | }, 50 | "valid": { 51 | fileName: validAuthProto, 52 | wantErr: nil, 53 | }, 54 | } 55 | 56 | for name, tc := range tests { 57 | name, tc := name, tc 58 | t.Run(name, func(t *testing.T) { 59 | t.Parallel() 60 | 61 | r, protos := start(t) 62 | 63 | rule := rules.CommentMessage{} 64 | issues, err := rule.Validate(protos[tc.fileName]) 65 | r.ErrorIs(err, tc.wantErr) 66 | switch { 67 | case tc.wantIssues != nil: 68 | r.Contains(issues, *tc.wantIssues) 69 | case len(issues) > 0: 70 | r.Empty(issues) 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /internal/rules/comment_one_of.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | var _ core.Rule = (*CommentOneof)(nil) 8 | 9 | // CommentOneof this rule checks that oneofs have non-empty comments. 10 | type CommentOneof struct{} 11 | 12 | // Message implements lint.Rule. 13 | func (c *CommentOneof) Message() string { 14 | return "oneof comments must not be empty" 15 | } 16 | 17 | // Validate implements lint.Rule. 18 | func (c *CommentOneof) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 19 | var res []core.Issue 20 | 21 | for _, msg := range protoInfo.Info.ProtoBody.Messages { 22 | for _, oneof := range msg.MessageBody.Oneofs { 23 | if len(oneof.Comments) == 0 { 24 | res = core.AppendIssue(res, c, oneof.Meta.Pos, oneof.OneofName, oneof.Comments) 25 | } 26 | } 27 | } 28 | 29 | return res, nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/rules/comment_one_of_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/rules" 10 | 11 | "github.com/easyp-tech/easyp/internal/core" 12 | ) 13 | 14 | func TestCommentOneof_Message(t *testing.T) { 15 | t.Parallel() 16 | 17 | assert := require.New(t) 18 | 19 | const expMessage = "oneof comments must not be empty" 20 | 21 | rule := rules.CommentOneof{} 22 | message := rule.Message() 23 | 24 | assert.Equal(expMessage, message) 25 | } 26 | 27 | func TestCommentOneOf_Validate(t *testing.T) { 28 | t.Parallel() 29 | 30 | tests := map[string]struct { 31 | fileName string 32 | wantIssues *core.Issue 33 | wantErr error 34 | }{ 35 | "invalid": { 36 | fileName: invalidAuthProto, 37 | wantIssues: &core.Issue{ 38 | Position: meta.Position{ 39 | Filename: "", 40 | Offset: 748, 41 | Line: 39, 42 | Column: 3, 43 | }, 44 | SourceName: "SocialNetwork", 45 | Message: "oneof comments must not be empty", 46 | RuleName: "COMMENT_ONEOF", 47 | }, 48 | wantErr: nil, 49 | }, 50 | "valid": { 51 | fileName: validAuthProto, 52 | wantErr: nil, 53 | }, 54 | } 55 | 56 | for name, tc := range tests { 57 | name, tc := name, tc 58 | t.Run(name, func(t *testing.T) { 59 | t.Parallel() 60 | 61 | r, protos := start(t) 62 | 63 | rule := rules.CommentOneof{} 64 | issues, err := rule.Validate(protos[tc.fileName]) 65 | r.ErrorIs(err, tc.wantErr) 66 | switch { 67 | case tc.wantIssues != nil: 68 | r.Contains(issues, *tc.wantIssues) 69 | case len(issues) > 0: 70 | r.Empty(issues) 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /internal/rules/comment_rpc.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | var _ core.Rule = (*CommentRPC)(nil) 8 | 9 | // CommentRPC this rule checks that RPCs have non-empty comments. 10 | type CommentRPC struct{} 11 | 12 | // Message implements lint.Rule. 13 | func (c *CommentRPC) Message() string { 14 | return "rpc comments must not be empty" 15 | } 16 | 17 | // Validate implements lint.Rule. 18 | func (c *CommentRPC) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 19 | var res []core.Issue 20 | 21 | for _, service := range protoInfo.Info.ProtoBody.Services { 22 | for _, rpc := range service.ServiceBody.RPCs { 23 | if len(service.Comments) == 0 { 24 | res = core.AppendIssue(res, c, rpc.Meta.Pos, rpc.RPCName, service.Comments) 25 | } 26 | } 27 | } 28 | 29 | return res, nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/rules/comment_rpc_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestCommentRPC_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "rpc comments must not be empty" 19 | 20 | rule := rules.CommentRPC{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestCommentRPC_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 214, 40 | Line: 11, 41 | Column: 3, 42 | }, 43 | SourceName: "Save", 44 | Message: "rpc comments must not be empty", 45 | RuleName: "COMMENT_RPC", 46 | }, 47 | wantErr: nil, 48 | }, 49 | "valid": { 50 | fileName: validAuthProto, 51 | wantErr: nil, 52 | }, 53 | } 54 | 55 | for name, tc := range tests { 56 | name, tc := name, tc 57 | t.Run(name, func(t *testing.T) { 58 | t.Parallel() 59 | 60 | r, protos := start(t) 61 | 62 | rule := rules.CommentRPC{} 63 | issues, err := rule.Validate(protos[tc.fileName]) 64 | r.ErrorIs(err, tc.wantErr) 65 | switch { 66 | case tc.wantIssues != nil: 67 | r.Contains(issues, *tc.wantIssues) 68 | case len(issues) > 0: 69 | r.Empty(issues) 70 | } 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/rules/comment_service.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | var _ core.Rule = (*CommentService)(nil) 8 | 9 | // CommentService this rule checks that services have non-empty comments. 10 | type CommentService struct{} 11 | 12 | // Message implements lint.Rule. 13 | func (c *CommentService) Message() string { 14 | return "service comments must not be empty" 15 | } 16 | 17 | // Validate implements lint.Rule. 18 | func (c *CommentService) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 19 | var res []core.Issue 20 | 21 | for _, service := range protoInfo.Info.ProtoBody.Services { 22 | if len(service.Comments) == 0 { 23 | res = core.AppendIssue(res, c, service.Meta.Pos, service.ServiceName, service.Comments) 24 | } 25 | } 26 | 27 | return res, nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/rules/comment_service_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestCommentService_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "service comments must not be empty" 19 | 20 | rule := rules.CommentService{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestCommentService_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 197, 40 | Line: 10, 41 | Column: 1, 42 | }, 43 | SourceName: "auth", 44 | Message: "service comments must not be empty", 45 | RuleName: "COMMENT_SERVICE", 46 | }, 47 | wantErr: nil, 48 | }, 49 | "valid": { 50 | fileName: validAuthProto, 51 | wantErr: nil, 52 | }, 53 | } 54 | 55 | for name, tc := range tests { 56 | name, tc := name, tc 57 | t.Run(name, func(t *testing.T) { 58 | t.Parallel() 59 | 60 | r, protos := start(t) 61 | 62 | rule := rules.CommentService{} 63 | issues, err := rule.Validate(protos[tc.fileName]) 64 | r.ErrorIs(err, tc.wantErr) 65 | switch { 66 | case tc.wantIssues != nil: 67 | r.Contains(issues, *tc.wantIssues) 68 | case len(issues) > 0: 69 | r.Empty(issues) 70 | } 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/rules/directory_same_package.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/easyp-tech/easyp/internal/core" 7 | ) 8 | 9 | var _ core.Rule = (*DirectorySamePackage)(nil) 10 | 11 | // DirectorySamePackage this rule checks that all files in a given directory are in the same package. 12 | type DirectorySamePackage struct { 13 | // dir => package 14 | cache map[string]string 15 | } 16 | 17 | func (d *DirectorySamePackage) lazyInit() { 18 | if d.cache == nil { 19 | d.cache = make(map[string]string) 20 | } 21 | } 22 | 23 | // Message implements lint.Rule. 24 | func (d *DirectorySamePackage) Message() string { 25 | return "all files in the same directory must have the same package name" 26 | } 27 | 28 | // Validate implements lint.Rule. 29 | func (d *DirectorySamePackage) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 30 | d.lazyInit() 31 | var res []core.Issue 32 | 33 | directory := filepath.Dir(protoInfo.Path) 34 | for _, pack := range protoInfo.Info.ProtoBody.Packages { 35 | if d.cache[directory] == "" { 36 | d.cache[directory] = pack.Name 37 | continue 38 | } 39 | 40 | if d.cache[directory] != pack.Name { 41 | res = core.AppendIssue(res, d, pack.Meta.Pos, pack.Name, pack.Comments) 42 | } 43 | } 44 | 45 | return res, nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/rules/directory_same_package_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestDirectorySamePackage_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "all files in the same directory must have the same package name" 19 | 20 | rule := rules.DirectorySamePackage{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | 25 | } 26 | func TestDirectorySamePackage_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileNames []string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileNames: []string{invalidAuthProto, invalidAuthProto2}, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 20, 40 | Line: 3, 41 | Column: 1, 42 | }, 43 | SourceName: "Queue", 44 | Message: "all files in the same directory must have the same package name", 45 | RuleName: "DIRECTORY_SAME_PACKAGE", 46 | }, 47 | wantErr: nil, 48 | }, 49 | "valid": { 50 | fileNames: []string{validAuthProto, validAuthProto2}, 51 | wantErr: nil, 52 | }, 53 | } 54 | 55 | for name, tc := range tests { 56 | name, tc := name, tc 57 | t.Run(name, func(t *testing.T) { 58 | t.Parallel() 59 | 60 | r, protos := start(t) 61 | 62 | rule := rules.DirectorySamePackage{} 63 | var res []core.Issue 64 | for _, fileName := range tc.fileNames { 65 | issues, err := rule.Validate(protos[fileName]) 66 | r.ErrorIs(err, tc.wantErr) 67 | res = append(res, issues...) 68 | } 69 | if len(res) > 0 { 70 | r.Contains(res, *tc.wantIssues) 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /internal/rules/enum_first_value_zero.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | var _ core.Rule = (*EnumFirstValueZero)(nil) 8 | 9 | // EnumFirstValueZero this rule enforces that the first enum value is the zero value, 10 | // which is a proto3 requirement on build, 11 | // but isn't required in proto2 on build. The rule enforces that the requirement is also followed in proto2. 12 | type EnumFirstValueZero struct{} 13 | 14 | // Message implements lint.Rule. 15 | func (c *EnumFirstValueZero) Message() string { 16 | return "enum first value must be zero" 17 | } 18 | 19 | // Validate implements lint.Rule. 20 | func (c *EnumFirstValueZero) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 21 | var res []core.Issue 22 | for _, enum := range protoInfo.Info.ProtoBody.Enums { 23 | if val := enum.EnumBody.EnumFields[0]; val.Number != "0" { 24 | res = core.AppendIssue(res, c, val.Meta.Pos, val.Number, val.Comments) 25 | } 26 | } 27 | 28 | for _, msg := range protoInfo.Info.ProtoBody.Messages { 29 | for _, enum := range msg.MessageBody.Enums { 30 | if val := enum.EnumBody.EnumFields[0]; val.Number != "0" { 31 | res = core.AppendIssue(res, c, val.Meta.Pos, val.Number, val.Comments) 32 | } 33 | } 34 | } 35 | 36 | return res, nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/rules/enum_first_value_zero_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestEnumFirstValueZero_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "enum first value must be zero" 19 | 20 | rule := rules.EnumFirstValueZero{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestEnumFirstValueZero_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 917, 40 | Line: 51, 41 | Column: 3, 42 | }, 43 | SourceName: "4", 44 | Message: "enum first value must be zero", 45 | RuleName: "ENUM_FIRST_VALUE_ZERO", 46 | }, 47 | wantErr: nil, 48 | }, 49 | "invalid_nested": { 50 | fileName: invalidAuthProto, 51 | wantIssues: &core.Issue{ 52 | Position: meta.Position{ 53 | Filename: "", 54 | Offset: 667, 55 | Line: 33, 56 | Column: 5, 57 | }, 58 | SourceName: "4", 59 | Message: "enum first value must be zero", 60 | RuleName: "ENUM_FIRST_VALUE_ZERO", 61 | }, 62 | wantErr: nil, 63 | }, 64 | "valid": { 65 | fileName: validAuthProto, 66 | wantErr: nil, 67 | }, 68 | } 69 | 70 | for name, tc := range tests { 71 | name, tc := name, tc 72 | t.Run(name, func(t *testing.T) { 73 | t.Parallel() 74 | 75 | r, protos := start(t) 76 | 77 | rule := rules.EnumFirstValueZero{} 78 | issues, err := rule.Validate(protos[tc.fileName]) 79 | r.ErrorIs(err, tc.wantErr) 80 | switch { 81 | case tc.wantIssues != nil: 82 | r.Contains(issues, *tc.wantIssues) 83 | case len(issues) > 0: 84 | r.Empty(issues) 85 | } 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/rules/enum_no_allow_alias.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | // EnumNoAllowAlias this rule checks that enums are PascalCase. 8 | type EnumNoAllowAlias struct{} 9 | 10 | // Message implements lint.Rule. 11 | func (e *EnumNoAllowAlias) Message() string { 12 | return "enum must not allow alias" 13 | } 14 | 15 | // Validate implements lint.Rule. 16 | func (e *EnumNoAllowAlias) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 17 | var res []core.Issue 18 | for _, enum := range protoInfo.Info.ProtoBody.Enums { 19 | for _, opt := range enum.EnumBody.Options { 20 | if opt.OptionName == "allow_alias" { 21 | res = core.AppendIssue(res, e, enum.Meta.Pos, enum.EnumName, enum.Comments) 22 | } 23 | } 24 | } 25 | 26 | for _, msg := range protoInfo.Info.ProtoBody.Messages { 27 | for _, enum := range msg.MessageBody.Enums { 28 | for _, opt := range enum.EnumBody.Options { 29 | if opt.OptionName == "allow_alias" { 30 | res = core.AppendIssue(res, e, enum.Meta.Pos, enum.EnumName, enum.Comments) 31 | } 32 | } 33 | } 34 | } 35 | 36 | return res, nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/rules/enum_no_allow_alias_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestEnumNoAllowAlias_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "enum must not allow alias" 19 | 20 | rule := rules.EnumNoAllowAlias{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestEnumNoAllowAlias_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 864, 40 | Line: 49, 41 | Column: 1, 42 | }, 43 | SourceName: "social_network", 44 | Message: "enum must not allow alias", 45 | RuleName: "ENUM_NO_ALLOW_ALIAS", 46 | }, 47 | wantErr: nil, 48 | }, 49 | "invalid_nested": { 50 | fileName: invalidAuthProto, 51 | wantIssues: &core.Issue{ 52 | Position: meta.Position{ 53 | Filename: "", 54 | Offset: 610, 55 | Line: 31, 56 | Column: 3, 57 | }, 58 | SourceName: "social_network", 59 | Message: "enum must not allow alias", 60 | RuleName: "ENUM_NO_ALLOW_ALIAS", 61 | }, 62 | wantErr: nil, 63 | }, 64 | "valid": { 65 | fileName: validAuthProto, 66 | wantErr: nil, 67 | }, 68 | } 69 | 70 | for name, tc := range tests { 71 | name, tc := name, tc 72 | t.Run(name, func(t *testing.T) { 73 | t.Parallel() 74 | 75 | r, protos := start(t) 76 | 77 | rule := rules.EnumNoAllowAlias{} 78 | issues, err := rule.Validate(protos[tc.fileName]) 79 | r.ErrorIs(err, tc.wantErr) 80 | switch { 81 | case tc.wantIssues != nil: 82 | r.Contains(issues, *tc.wantIssues) 83 | case len(issues) > 0: 84 | r.Empty(issues) 85 | } 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/rules/enum_pascal_case.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/easyp-tech/easyp/internal/core" 7 | ) 8 | 9 | var _ core.Rule = (*EnumPascalCase)(nil) 10 | 11 | // EnumPascalCase this rule checks that enums are PascalCase. 12 | type EnumPascalCase struct{} 13 | 14 | // Message implements lint.Rule. 15 | func (c *EnumPascalCase) Message() string { 16 | return "enum name must be in PascalCase" 17 | } 18 | 19 | // Validate implements lint.Rule. 20 | func (c *EnumPascalCase) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 21 | var res []core.Issue 22 | pascalCase := regexp.MustCompile("^[A-Z][a-z]+(?:[A-Z][a-z]+)*$") 23 | for _, enum := range protoInfo.Info.ProtoBody.Enums { 24 | if !pascalCase.MatchString(enum.EnumName) { 25 | res = core.AppendIssue(res, c, enum.Meta.Pos, enum.EnumName, enum.Comments) 26 | } 27 | } 28 | 29 | for _, msg := range protoInfo.Info.ProtoBody.Messages { 30 | for _, enum := range msg.MessageBody.Enums { 31 | if !pascalCase.MatchString(enum.EnumName) { 32 | res = core.AppendIssue(res, c, enum.Meta.Pos, enum.EnumName, enum.Comments) 33 | } 34 | } 35 | } 36 | 37 | return res, nil 38 | } 39 | -------------------------------------------------------------------------------- /internal/rules/enum_pascal_case_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestEnumPascalCase_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "enum name must be in PascalCase" 19 | 20 | rule := rules.EnumPascalCase{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestEnumPascalCase_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 864, 40 | Line: 49, 41 | Column: 1, 42 | }, 43 | SourceName: "social_network", 44 | Message: "enum name must be in PascalCase", 45 | RuleName: "ENUM_PASCAL_CASE", 46 | }, 47 | wantErr: nil, 48 | }, 49 | "invalid_nested": { 50 | fileName: invalidAuthProto, 51 | wantIssues: &core.Issue{ 52 | Position: meta.Position{ 53 | Filename: "", 54 | Offset: 610, 55 | Line: 31, 56 | Column: 3, 57 | }, 58 | SourceName: "social_network", 59 | Message: "enum name must be in PascalCase", 60 | RuleName: "ENUM_PASCAL_CASE", 61 | }, 62 | wantErr: nil, 63 | }, 64 | "valid": { 65 | fileName: validAuthProto, 66 | wantErr: nil, 67 | }, 68 | } 69 | 70 | for name, tc := range tests { 71 | name, tc := name, tc 72 | t.Run(name, func(t *testing.T) { 73 | t.Parallel() 74 | 75 | r, protos := start(t) 76 | 77 | rule := rules.EnumPascalCase{} 78 | issues, err := rule.Validate(protos[tc.fileName]) 79 | r.ErrorIs(err, tc.wantErr) 80 | switch { 81 | case tc.wantIssues != nil: 82 | r.Contains(issues, *tc.wantIssues) 83 | case len(issues) > 0: 84 | r.Empty(issues) 85 | } 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/rules/enum_value_prefix.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "strings" 5 | "unicode" 6 | 7 | "github.com/easyp-tech/easyp/internal/core" 8 | ) 9 | 10 | var _ core.Rule = (*EnumValuePrefix)(nil) 11 | 12 | // EnumValuePrefix this rule requires that all enum value names are prefixed with the enum name. 13 | type EnumValuePrefix struct { 14 | } 15 | 16 | // Message implements lint.Rule. 17 | func (e *EnumValuePrefix) Message() string { 18 | return "enum value prefix is not valid" 19 | } 20 | 21 | // Validate implements lint.Rule. 22 | func (e *EnumValuePrefix) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 23 | var res []core.Issue 24 | 25 | for _, enum := range protoInfo.Info.ProtoBody.Enums { 26 | prefix := pascalToUpperSnake(enum.EnumName) 27 | 28 | for _, enumValue := range enum.EnumBody.EnumFields { 29 | if !strings.HasPrefix(enumValue.Ident, prefix) { 30 | res = core.AppendIssue( 31 | res, 32 | e, 33 | enumValue.Meta.Pos, 34 | enumValue.Ident, 35 | enumValue.Comments, 36 | ) 37 | } 38 | } 39 | } 40 | 41 | for _, msg := range protoInfo.Info.ProtoBody.Messages { 42 | for _, enum := range msg.MessageBody.Enums { 43 | prefix := pascalToUpperSnake(enum.EnumName) 44 | 45 | for _, enumValue := range enum.EnumBody.EnumFields { 46 | if !strings.HasPrefix(enumValue.Ident, prefix) { 47 | res = core.AppendIssue( 48 | res, 49 | e, 50 | enumValue.Meta.Pos, 51 | enumValue.Ident, 52 | enumValue.Comments, 53 | ) 54 | } 55 | } 56 | } 57 | } 58 | 59 | return res, nil 60 | } 61 | 62 | func pascalToUpperSnake(s string) string { 63 | var result string 64 | 65 | for _, char := range s { 66 | if unicode.IsUpper(char) { 67 | if len(result) > 0 { 68 | result += "_" 69 | } 70 | result += string(char) 71 | } else { 72 | result += string(unicode.ToUpper(char)) 73 | } 74 | } 75 | 76 | return result 77 | } 78 | -------------------------------------------------------------------------------- /internal/rules/enum_value_prefix_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestEnumValuePrefix_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "enum value prefix is not valid" 19 | 20 | rule := rules.EnumValuePrefix{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestEnumValuePrefix_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 917, 40 | Line: 51, 41 | Column: 3, 42 | }, 43 | SourceName: "none", 44 | Message: "enum value prefix is not valid", 45 | RuleName: "ENUM_VALUE_PREFIX", 46 | }, 47 | wantErr: nil, 48 | }, 49 | "invalid_nested": { 50 | fileName: invalidAuthProto, 51 | wantIssues: &core.Issue{ 52 | Position: meta.Position{ 53 | Filename: "", 54 | Offset: 667, 55 | Line: 33, 56 | Column: 5, 57 | }, 58 | SourceName: "none", 59 | Message: "enum value prefix is not valid", 60 | RuleName: "ENUM_VALUE_PREFIX", 61 | }, 62 | wantErr: nil, 63 | }, 64 | "valid": { 65 | fileName: validAuthProto, 66 | wantErr: nil, 67 | }, 68 | } 69 | 70 | for name, tc := range tests { 71 | name, tc := name, tc 72 | t.Run(name, func(t *testing.T) { 73 | t.Parallel() 74 | 75 | r, protos := start(t) 76 | 77 | rule := rules.EnumValuePrefix{} 78 | issues, err := rule.Validate(protos[tc.fileName]) 79 | r.ErrorIs(err, tc.wantErr) 80 | switch { 81 | case tc.wantIssues != nil: 82 | r.Contains(issues, *tc.wantIssues) 83 | case len(issues) > 0: 84 | r.Empty(issues) 85 | } 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/rules/enum_value_upper_snake_case.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/easyp-tech/easyp/internal/core" 7 | ) 8 | 9 | var _ core.Rule = (*EnumValueUpperSnakeCase)(nil) 10 | 11 | // EnumValueUpperSnakeCase this rule checks that enum values are UPPER_SNAKE_CASE. 12 | type EnumValueUpperSnakeCase struct{} 13 | 14 | // Message implements lint.Rule. 15 | func (c *EnumValueUpperSnakeCase) Message() string { 16 | return "enum value must be in UPPER_SNAKE_CASE" 17 | } 18 | 19 | // Validate implements lint.Rule. 20 | func (c *EnumValueUpperSnakeCase) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 21 | var res []core.Issue 22 | upperSnakeCase := regexp.MustCompile("^[A-Z0-9]+(_[A-Z0-9]+)*$") 23 | for _, enum := range protoInfo.Info.ProtoBody.Enums { 24 | for _, field := range enum.EnumBody.EnumFields { 25 | if !upperSnakeCase.MatchString(field.Ident) { 26 | res = core.AppendIssue(res, c, field.Meta.Pos, field.Ident, field.Comments) 27 | } 28 | } 29 | } 30 | 31 | for _, msg := range protoInfo.Info.ProtoBody.Messages { 32 | for _, enum := range msg.MessageBody.Enums { 33 | for _, field := range enum.EnumBody.EnumFields { 34 | if !upperSnakeCase.MatchString(field.Ident) { 35 | res = core.AppendIssue(res, c, field.Meta.Pos, field.Ident, field.Comments) 36 | } 37 | } 38 | } 39 | } 40 | 41 | return res, nil 42 | } 43 | -------------------------------------------------------------------------------- /internal/rules/enum_value_upper_snake_case_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestEnumValueUpperSnakeCase_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "enum value must be in UPPER_SNAKE_CASE" 19 | 20 | rule := rules.EnumValueUpperSnakeCase{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestEnumValueUpperSnakeCase_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 917, 40 | Line: 51, 41 | Column: 3, 42 | }, 43 | SourceName: "none", 44 | Message: "enum value must be in UPPER_SNAKE_CASE", 45 | RuleName: "ENUM_VALUE_UPPER_SNAKE_CASE", 46 | }, 47 | wantErr: nil, 48 | }, 49 | "invalid_nested": { 50 | fileName: invalidAuthProto, 51 | wantIssues: &core.Issue{ 52 | Position: meta.Position{ 53 | Filename: "", 54 | Offset: 667, 55 | Line: 33, 56 | Column: 5, 57 | }, 58 | SourceName: "none", 59 | Message: "enum value must be in UPPER_SNAKE_CASE", 60 | RuleName: "ENUM_VALUE_UPPER_SNAKE_CASE", 61 | }, 62 | wantErr: nil, 63 | }, 64 | "valid": { 65 | fileName: validAuthProto, 66 | wantErr: nil, 67 | }, 68 | } 69 | 70 | for name, tc := range tests { 71 | name, tc := name, tc 72 | t.Run(name, func(t *testing.T) { 73 | t.Parallel() 74 | 75 | r, protos := start(t) 76 | 77 | rule := rules.EnumValueUpperSnakeCase{} 78 | issues, err := rule.Validate(protos[tc.fileName]) 79 | r.ErrorIs(err, tc.wantErr) 80 | switch { 81 | case tc.wantIssues != nil: 82 | r.Contains(issues, *tc.wantIssues) 83 | case len(issues) > 0: 84 | r.Empty(issues) 85 | } 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/rules/enum_zero_value_suffix.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | var _ core.Rule = (*EnumZeroValueSuffix)(nil) 8 | 9 | // EnumZeroValueSuffix this rule requires that all enum values have a zero value with a defined suffix. 10 | // By default, it verifies that the zero value of all enums ends in _UNSPECIFIED, but the suffix is configurable. 11 | type EnumZeroValueSuffix struct { 12 | Suffix string `json:"suffix" yaml:"suffix" ENV:"ENUM_ZERO_VALUE_SUFFIX"` 13 | } 14 | 15 | // Message implements lint.Rule. 16 | func (e *EnumZeroValueSuffix) Message() string { 17 | return "enum zero value suffix is not valid" 18 | } 19 | 20 | // Validate implements lint.Rule. 21 | func (e *EnumZeroValueSuffix) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 22 | var res []core.Issue 23 | 24 | for _, enum := range protoInfo.Info.ProtoBody.Enums { 25 | zeroValue := enum.EnumBody.EnumFields[0] 26 | if zeroValue.Ident != pascalToUpperSnake(enum.EnumName)+"_"+e.Suffix { 27 | res = core.AppendIssue( 28 | res, 29 | e, 30 | zeroValue.Meta.Pos, 31 | zeroValue.Ident, 32 | zeroValue.Comments, 33 | ) 34 | } 35 | } 36 | 37 | for _, msg := range protoInfo.Info.ProtoBody.Messages { 38 | for _, enum := range msg.MessageBody.Enums { 39 | zeroValue := enum.EnumBody.EnumFields[0] 40 | if zeroValue.Ident != pascalToUpperSnake(enum.EnumName)+"_"+e.Suffix { 41 | res = core.AppendIssue( 42 | res, 43 | e, 44 | zeroValue.Meta.Pos, 45 | zeroValue.Ident, 46 | zeroValue.Comments, 47 | ) 48 | } 49 | } 50 | } 51 | 52 | return res, nil 53 | } 54 | -------------------------------------------------------------------------------- /internal/rules/enum_zero_value_suffix_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestEnumZeroValueSuffix_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "enum zero value suffix is not valid" 19 | 20 | rule := rules.EnumZeroValueSuffix{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestEnumZeroValueSuffix_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 917, 40 | Line: 51, 41 | Column: 3, 42 | }, 43 | SourceName: "none", 44 | Message: "enum zero value suffix is not valid", 45 | RuleName: "ENUM_ZERO_VALUE_SUFFIX", 46 | }, 47 | wantErr: nil, 48 | }, 49 | "invalid_nested": { 50 | fileName: invalidAuthProto, 51 | wantIssues: &core.Issue{ 52 | Position: meta.Position{ 53 | Filename: "", 54 | Offset: 667, 55 | Line: 33, 56 | Column: 5, 57 | }, 58 | SourceName: "none", 59 | Message: "enum zero value suffix is not valid", 60 | RuleName: "ENUM_ZERO_VALUE_SUFFIX", 61 | }, 62 | wantErr: nil, 63 | }, 64 | "valid": { 65 | fileName: validAuthProto, 66 | wantErr: nil, 67 | }, 68 | } 69 | 70 | for name, tc := range tests { 71 | name, tc := name, tc 72 | t.Run(name, func(t *testing.T) { 73 | t.Parallel() 74 | 75 | r, protos := start(t) 76 | 77 | rule := rules.EnumZeroValueSuffix{ 78 | Suffix: "NONE", 79 | } 80 | issues, err := rule.Validate(protos[tc.fileName]) 81 | r.ErrorIs(err, tc.wantErr) 82 | switch { 83 | case tc.wantIssues != nil: 84 | r.Contains(issues, *tc.wantIssues) 85 | case len(issues) > 0: 86 | r.Empty(issues) 87 | } 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /internal/rules/file_lower_snake_case.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "path/filepath" 5 | "regexp" 6 | 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | ) 11 | 12 | var _ core.Rule = (*FileLowerSnakeCase)(nil) 13 | 14 | // FileLowerSnakeCase this rule says that all .proto files must be named as lower_snake_case.proto. 15 | // This is the widely accepted standard. 16 | type FileLowerSnakeCase struct { 17 | } 18 | 19 | // Message implements lint.Rule. 20 | func (f *FileLowerSnakeCase) Message() string { 21 | return "file name should be lower_snake_case.proto" 22 | } 23 | 24 | // Validate implements lint.Rule. 25 | func (f *FileLowerSnakeCase) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 26 | var res []core.Issue 27 | 28 | fileName := filepath.Base(protoInfo.Path) 29 | if !isLowerSnakeCase(fileName) { 30 | res = core.AppendIssue(res, f, meta.Position{ 31 | Filename: protoInfo.Path, 32 | Offset: 0, 33 | Line: 0, 34 | Column: 0, 35 | }, protoInfo.Path, nil) 36 | } 37 | 38 | return res, nil 39 | } 40 | 41 | var matchLowerSnakeCase = regexp.MustCompile("^[a-z]+([_|[.][a-z0-9]+)*$") 42 | 43 | func isLowerSnakeCase(s string) bool { 44 | return matchLowerSnakeCase.MatchString(s) 45 | } 46 | -------------------------------------------------------------------------------- /internal/rules/file_lower_snake_case_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestFileLowerSnakeCase_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "file name should be lower_snake_case.proto" 19 | 20 | rule := rules.FileLowerSnakeCase{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestFileLowerSnakeCase_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto3, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "./../../testdata/auth/InvalidName.proto", 39 | Offset: 0, 40 | Line: 0, 41 | Column: 0, 42 | }, 43 | SourceName: "./../../testdata/auth/InvalidName.proto", 44 | Message: "file name should be lower_snake_case.proto", 45 | RuleName: "FILE_LOWER_SNAKE_CASE", 46 | }, 47 | wantErr: nil, 48 | }, 49 | "valid": { 50 | fileName: validAuthProto, 51 | wantErr: nil, 52 | }, 53 | } 54 | 55 | for name, tc := range tests { 56 | name, tc := name, tc 57 | t.Run(name, func(t *testing.T) { 58 | t.Parallel() 59 | 60 | r, protos := start(t) 61 | 62 | rule := rules.FileLowerSnakeCase{} 63 | issues, err := rule.Validate(protos[tc.fileName]) 64 | r.ErrorIs(err, tc.wantErr) 65 | switch { 66 | case tc.wantIssues != nil: 67 | r.Contains(issues, *tc.wantIssues) 68 | case len(issues) > 0: 69 | r.Empty(issues) 70 | } 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/rules/import_no_public.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/yoheimuta/go-protoparser/v4/parser" 5 | 6 | "github.com/easyp-tech/easyp/internal/core" 7 | ) 8 | 9 | var _ core.Rule = (*ImportNoPublic)(nil) 10 | 11 | // ImportNoPublic this rule outlaws declaring imports as public. 12 | // If you didn't know that was possible, forget what you just learned in this sentence. 13 | type ImportNoPublic struct{} 14 | 15 | // Message implements lint.Rule. 16 | func (i *ImportNoPublic) Message() string { 17 | return "import should not be public" 18 | } 19 | 20 | // Validate implements lint.Rule. 21 | func (i *ImportNoPublic) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 22 | var res []core.Issue 23 | 24 | for _, imp := range protoInfo.Info.ProtoBody.Imports { 25 | if imp.Modifier == parser.ImportModifierPublic { 26 | res = core.AppendIssue(res, i, imp.Meta.Pos, imp.Location, imp.Comments) 27 | } 28 | } 29 | 30 | return res, nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/rules/import_no_public_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestImportNoPublic_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "import should not be public" 19 | 20 | rule := rules.ImportNoPublic{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestImportNoPublic_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 81, 40 | Line: 6, 41 | Column: 1, 42 | }, 43 | SourceName: `"google/rpc/code.proto"`, 44 | Message: "import should not be public", 45 | RuleName: "IMPORT_NO_PUBLIC", 46 | }, 47 | wantErr: nil, 48 | }, 49 | "valid": { 50 | fileName: validAuthProto, 51 | wantErr: nil, 52 | }, 53 | } 54 | 55 | for name, tc := range tests { 56 | name, tc := name, tc 57 | t.Run(name, func(t *testing.T) { 58 | t.Parallel() 59 | 60 | r, protos := start(t) 61 | 62 | rule := rules.ImportNoPublic{} 63 | issues, err := rule.Validate(protos[tc.fileName]) 64 | r.ErrorIs(err, tc.wantErr) 65 | switch { 66 | case tc.wantIssues != nil: 67 | r.Contains(issues, *tc.wantIssues) 68 | case len(issues) > 0: 69 | r.Empty(issues) 70 | } 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/rules/import_no_weak.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/yoheimuta/go-protoparser/v4/parser" 5 | 6 | "github.com/easyp-tech/easyp/internal/core" 7 | ) 8 | 9 | var _ core.Rule = (*ImportNoWeak)(nil) 10 | 11 | // ImportNoWeak similar to the IMPORT_NO_PUBLIC rule, this rule outlaws declaring imports as weak. 12 | // If you didn't know that was possible, forget what you just learned in this sentence. 13 | type ImportNoWeak struct{} 14 | 15 | // Message implements lint.Rule. 16 | func (i *ImportNoWeak) Message() string { 17 | return "import should not be weak" 18 | } 19 | 20 | // Validate implements lint.Rule. 21 | func (i *ImportNoWeak) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 22 | var res []core.Issue 23 | 24 | for _, imp := range protoInfo.Info.ProtoBody.Imports { 25 | if imp.Modifier == parser.ImportModifierWeak { 26 | res = core.AppendIssue(res, i, imp.Meta.Pos, imp.Location, imp.Comments) 27 | } 28 | } 29 | 30 | return res, nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/rules/import_no_weak_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestImportNoWeak_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "import should not be weak" 19 | 20 | rule := rules.ImportNoWeak{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestImportNoWeak_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | 38 | Position: meta.Position{ 39 | Filename: "", 40 | Offset: 38, 41 | Line: 5, 42 | Column: 1, 43 | }, 44 | SourceName: `"google/protobuf/empty.proto"`, 45 | Message: "import should not be weak", 46 | RuleName: "IMPORT_NO_WEAK", 47 | }, 48 | wantErr: nil, 49 | }, 50 | "valid": { 51 | fileName: validAuthProto, 52 | wantErr: nil, 53 | }, 54 | } 55 | 56 | for name, tc := range tests { 57 | name, tc := name, tc 58 | t.Run(name, func(t *testing.T) { 59 | t.Parallel() 60 | 61 | r, protos := start(t) 62 | 63 | rule := rules.ImportNoWeak{} 64 | issues, err := rule.Validate(protos[tc.fileName]) 65 | r.ErrorIs(err, tc.wantErr) 66 | switch { 67 | case tc.wantIssues != nil: 68 | r.Contains(issues, *tc.wantIssues) 69 | case len(issues) > 0: 70 | r.Empty(issues) 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /internal/rules/import_used_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestImportUsed_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "import is not used" 19 | 20 | rule := rules.ImportUsed{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestImportUsed_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: importNotUsed, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 20, 40 | Line: 3, 41 | Column: 1, 42 | }, 43 | SourceName: `"import_used/messages.proto"`, 44 | Message: "import is not used", 45 | RuleName: "IMPORT_USED", 46 | }, 47 | wantErr: nil, 48 | }, 49 | "valid": { 50 | fileName: importUsed, 51 | wantErr: nil, 52 | }, 53 | } 54 | 55 | for name, tc := range tests { 56 | name, tc := name, tc 57 | t.Run(name, func(t *testing.T) { 58 | t.Parallel() 59 | 60 | r, protos := start(t) 61 | 62 | rule := rules.ImportUsed{} 63 | issues, err := rule.Validate(protos[tc.fileName]) 64 | r.ErrorIs(err, tc.wantErr) 65 | switch { 66 | case tc.wantIssues != nil: 67 | r.Contains(issues, *tc.wantIssues) 68 | case len(issues) > 0: 69 | r.Empty(issues) 70 | } 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/rules/message_field_lower_snake_case.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/easyp-tech/easyp/internal/core" 7 | ) 8 | 9 | var _ core.Rule = (*FieldLowerSnakeCase)(nil) 10 | 11 | // FieldLowerSnakeCase this rule checks that field names are lower_snake_case. 12 | type FieldLowerSnakeCase struct{} 13 | 14 | // Message implements lint.Rule. 15 | func (c *FieldLowerSnakeCase) Message() string { 16 | return "message field should be lower_snake_case" 17 | } 18 | 19 | // Validate implements lint.Rule. 20 | func (c *FieldLowerSnakeCase) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 21 | var res []core.Issue 22 | 23 | lowerSnakeCase := regexp.MustCompile("^[a-z0-9]+(_[a-z0-9]+)*$") 24 | for _, message := range protoInfo.Info.ProtoBody.Messages { 25 | for _, field := range message.MessageBody.Fields { 26 | if !lowerSnakeCase.MatchString(field.FieldName) { 27 | res = core.AppendIssue(res, c, field.Meta.Pos, field.FieldName, field.Comments) 28 | } 29 | } 30 | } 31 | 32 | return res, nil 33 | } 34 | -------------------------------------------------------------------------------- /internal/rules/message_field_lower_snake_case_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestFieldLowerSnakeCase_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "message field should be lower_snake_case" 19 | 20 | rule := rules.FieldLowerSnakeCase{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestMessageFieldLowerSnakeCase_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 560, 40 | Line: 27, 41 | Column: 3, 42 | }, 43 | SourceName: "Session_id", 44 | Message: "message field should be lower_snake_case", 45 | RuleName: "FIELD_LOWER_SNAKE_CASE", 46 | }, 47 | wantErr: nil, 48 | }, 49 | "valid": { 50 | fileName: validAuthProto, 51 | wantErr: nil, 52 | }, 53 | } 54 | 55 | for name, tc := range tests { 56 | name, tc := name, tc 57 | t.Run(name, func(t *testing.T) { 58 | t.Parallel() 59 | 60 | r, protos := start(t) 61 | 62 | rule := rules.FieldLowerSnakeCase{} 63 | issues, err := rule.Validate(protos[tc.fileName]) 64 | r.ErrorIs(err, tc.wantErr) 65 | switch { 66 | case tc.wantIssues != nil: 67 | r.Contains(issues, *tc.wantIssues) 68 | case len(issues) > 0: 69 | r.Empty(issues) 70 | } 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/rules/message_pascal_case.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/easyp-tech/easyp/internal/core" 7 | ) 8 | 9 | var _ core.Rule = (*MessagePascalCase)(nil) 10 | 11 | // MessagePascalCase this rule checks that messages are PascalCase. 12 | type MessagePascalCase struct{} 13 | 14 | // Message implements lint.Rule. 15 | func (c *MessagePascalCase) Message() string { 16 | return "message name should be PascalCase" 17 | } 18 | 19 | // Validate implements lint.Rule. 20 | func (c *MessagePascalCase) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 21 | var res []core.Issue 22 | 23 | pascalCase := regexp.MustCompile("^[A-Z][a-zA-Z0-9]+(?:[A-Z][a-zA-Z0-9]+)*$") 24 | for _, message := range protoInfo.Info.ProtoBody.Messages { 25 | if !pascalCase.MatchString(message.MessageName) { 26 | res = core.AppendIssue(res, c, message.Meta.Pos, message.MessageName, message.Comments) 27 | } 28 | } 29 | 30 | return res, nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/rules/message_pascal_case_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestMessagePascalCase_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "message name should be PascalCase" 19 | 20 | rule := rules.MessagePascalCase{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestMessagePascalCase_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 536, 40 | Line: 26, 41 | Column: 1, 42 | }, 43 | SourceName: "Delete_Info", 44 | Message: "message name should be PascalCase", 45 | RuleName: "MESSAGE_PASCAL_CASE", 46 | }, 47 | wantErr: nil, 48 | }, 49 | "valid": { 50 | fileName: validAuthProto, 51 | wantErr: nil, 52 | }, 53 | } 54 | 55 | for name, tc := range tests { 56 | name, tc := name, tc 57 | t.Run(name, func(t *testing.T) { 58 | t.Parallel() 59 | 60 | r, protos := start(t) 61 | 62 | rule := rules.MessagePascalCase{} 63 | issues, err := rule.Validate(protos[tc.fileName]) 64 | r.ErrorIs(err, tc.wantErr) 65 | switch { 66 | case tc.wantIssues != nil: 67 | r.Contains(issues, *tc.wantIssues) 68 | case len(issues) > 0: 69 | r.Empty(issues) 70 | } 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/rules/oneof_lower_snake_case.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/easyp-tech/easyp/internal/core" 7 | ) 8 | 9 | var _ core.Rule = (*OneofLowerSnakeCase)(nil) 10 | 11 | // OneofLowerSnakeCase this rule checks that oneof names are lower_snake_case. 12 | type OneofLowerSnakeCase struct{} 13 | 14 | // Message implements lint.Rule. 15 | func (c *OneofLowerSnakeCase) Message() string { 16 | return "oneof name should be lower_snake_case" 17 | } 18 | 19 | // Validate implements lint.Rule. 20 | func (c *OneofLowerSnakeCase) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 21 | var res []core.Issue 22 | lowerSnakeCase := regexp.MustCompile("^[a-z]+(_[a-z]+)*$") 23 | for _, message := range protoInfo.Info.ProtoBody.Messages { 24 | for _, oneof := range message.MessageBody.Oneofs { 25 | if !lowerSnakeCase.MatchString(oneof.OneofName) { 26 | res = core.AppendIssue(res, c, oneof.Meta.Pos, oneof.OneofName, oneof.Comments) 27 | } 28 | } 29 | } 30 | 31 | return res, nil 32 | } 33 | -------------------------------------------------------------------------------- /internal/rules/oneof_lower_snake_case_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestOneofLowerSnakeCase_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "oneof name should be lower_snake_case" 19 | 20 | rule := rules.OneofLowerSnakeCase{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestOneofLowerSnakeCase_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 748, 40 | Line: 39, 41 | Column: 3, 42 | }, 43 | SourceName: "SocialNetwork", 44 | Message: "oneof name should be lower_snake_case", 45 | RuleName: "ONEOF_LOWER_SNAKE_CASE", 46 | }, 47 | wantErr: nil, 48 | }, 49 | "valid": { 50 | fileName: validAuthProto, 51 | wantErr: nil, 52 | }, 53 | } 54 | 55 | for name, tc := range tests { 56 | name, tc := name, tc 57 | t.Run(name, func(t *testing.T) { 58 | t.Parallel() 59 | 60 | r, protos := start(t) 61 | 62 | rule := rules.OneofLowerSnakeCase{} 63 | issues, err := rule.Validate(protos[tc.fileName]) 64 | r.ErrorIs(err, tc.wantErr) 65 | switch { 66 | case tc.wantIssues != nil: 67 | r.Contains(issues, *tc.wantIssues) 68 | case len(issues) > 0: 69 | r.Empty(issues) 70 | } 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/rules/package_defined.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 5 | 6 | "github.com/easyp-tech/easyp/internal/core" 7 | ) 8 | 9 | var _ core.Rule = (*PackageDefined)(nil) 10 | 11 | // PackageDefined this rule checks that all files have a package declaration. 12 | type PackageDefined struct{} 13 | 14 | // Message implements lint.Rule. 15 | func (p *PackageDefined) Message() string { 16 | return "package should be defined" 17 | } 18 | 19 | // Validate implements lint.Rule. 20 | func (p *PackageDefined) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 21 | var res []core.Issue 22 | 23 | if len(protoInfo.Info.ProtoBody.Packages) == 0 { 24 | res = core.AppendIssue(res, p, meta.Position{ 25 | Filename: protoInfo.Path, 26 | }, protoInfo.Path, nil) 27 | } 28 | 29 | return res, nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/rules/package_defined_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestPackageDefined_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "package should be defined" 19 | 20 | rule := rules.PackageDefined{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestPackageDefined_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProtoEmptyPkg, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "./../../testdata/auth/empty_pkg.proto", 39 | Offset: 0, 40 | Line: 0, 41 | Column: 0, 42 | }, 43 | SourceName: "./../../testdata/auth/empty_pkg.proto", 44 | Message: "package should be defined", 45 | RuleName: "PACKAGE_DEFINED", 46 | }, 47 | wantErr: nil, 48 | }, 49 | "valid": { 50 | fileName: validAuthProto, 51 | wantErr: nil, 52 | }, 53 | } 54 | 55 | for name, tc := range tests { 56 | name, tc := name, tc 57 | t.Run(name, func(t *testing.T) { 58 | t.Parallel() 59 | 60 | r, protos := start(t) 61 | 62 | rule := rules.PackageDefined{} 63 | issues, err := rule.Validate(protos[tc.fileName]) 64 | r.ErrorIs(err, tc.wantErr) 65 | switch { 66 | case tc.wantIssues != nil: 67 | r.Contains(issues, *tc.wantIssues) 68 | case len(issues) > 0: 69 | r.Empty(issues) 70 | } 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/rules/package_directory_match.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | 7 | "github.com/easyp-tech/easyp/internal/core" 8 | ) 9 | 10 | var _ core.Rule = (*PackageDirectoryMatch)(nil) 11 | 12 | // PackageDirectoryMatch is a rule for checking consistency of directory and package names. 13 | type PackageDirectoryMatch struct { 14 | Root string `json:"root" yaml:"root" env:"PACKAGE_DIRECTORY_MATCH_ROOT"` 15 | } 16 | 17 | // Message implements lint.Rule. 18 | func (d *PackageDirectoryMatch) Message() string { 19 | return "package is not matched with path" 20 | } 21 | 22 | // Validate implements lint.Rule. 23 | func (d *PackageDirectoryMatch) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 24 | var res []core.Issue 25 | 26 | preparePath := filepath.Dir(strings.TrimPrefix(protoInfo.Path, d.Root)) 27 | expectedPackage := strings.ReplaceAll(preparePath, "/", ".") 28 | 29 | for _, pkgInfo := range protoInfo.Info.ProtoBody.Packages { 30 | if pkgInfo.Name != expectedPackage { 31 | res = core.AppendIssue(res, d, pkgInfo.Meta.Pos, protoInfo.Path, pkgInfo.Comments) 32 | } 33 | } 34 | 35 | return res, nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/rules/package_directory_match_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestPackageDirectoryMatch_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "package is not matched with path" 19 | 20 | rule := rules.PackageDirectoryMatch{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestPackageDirectoryMatch_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 20, 40 | Line: 3, 41 | Column: 1, 42 | }, 43 | SourceName: "./../../testdata/auth/service.proto", 44 | Message: "package is not matched with path", 45 | RuleName: "PACKAGE_DIRECTORY_MATCH", 46 | }, 47 | wantErr: nil, 48 | }, 49 | "valid": { 50 | fileName: validAuthProto, 51 | wantErr: nil, 52 | }, 53 | } 54 | 55 | for name, tc := range tests { 56 | name, tc := name, tc 57 | t.Run(name, func(t *testing.T) { 58 | t.Parallel() 59 | 60 | r, protos := start(t) 61 | 62 | rule := rules.PackageDirectoryMatch{ 63 | Root: "./../../testdata/", 64 | } 65 | issues, err := rule.Validate(protos[tc.fileName]) 66 | r.ErrorIs(err, tc.wantErr) 67 | switch { 68 | case tc.wantIssues != nil: 69 | r.Contains(issues, *tc.wantIssues) 70 | case len(issues) > 0: 71 | r.Empty(issues) 72 | } 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /internal/rules/package_lower_snake_case.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/easyp-tech/easyp/internal/core" 7 | ) 8 | 9 | var _ core.Rule = (*PackageLowerSnakeCase)(nil) 10 | 11 | // PackageLowerSnakeCase his rule checks that packages are lower_snake_case. 12 | type PackageLowerSnakeCase struct{} 13 | 14 | // Message implements lint.Rule. 15 | func (c *PackageLowerSnakeCase) Message() string { 16 | return "package name should be lower_snake_case" 17 | } 18 | 19 | // Validate implements lint.Rule. 20 | func (c *PackageLowerSnakeCase) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 21 | var res []core.Issue 22 | lowerSnakeCase := regexp.MustCompile("^[a-z]+([_|[.][a-z0-9]+)*$") 23 | for _, pack := range protoInfo.Info.ProtoBody.Packages { 24 | if !lowerSnakeCase.MatchString(pack.Name) { 25 | res = core.AppendIssue(res, c, pack.Meta.Pos, pack.Name, pack.Comments) 26 | } 27 | } 28 | 29 | return res, nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/rules/package_lower_snake_case_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestPackageLowerSnakeCase_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "package name should be lower_snake_case" 19 | 20 | rule := rules.PackageLowerSnakeCase{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestPackageLowerSnakeCase_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 20, 40 | Line: 3, 41 | Column: 1, 42 | }, 43 | SourceName: "Session", 44 | Message: "package name should be lower_snake_case", 45 | RuleName: "PACKAGE_LOWER_SNAKE_CASE", 46 | }, 47 | wantErr: nil, 48 | }, 49 | "valid": { 50 | fileName: validAuthProto, 51 | wantErr: nil, 52 | }, 53 | } 54 | 55 | for name, tc := range tests { 56 | name, tc := name, tc 57 | t.Run(name, func(t *testing.T) { 58 | t.Parallel() 59 | 60 | r, protos := start(t) 61 | 62 | rule := rules.PackageLowerSnakeCase{} 63 | issues, err := rule.Validate(protos[tc.fileName]) 64 | r.ErrorIs(err, tc.wantErr) 65 | switch { 66 | case tc.wantIssues != nil: 67 | r.Contains(issues, *tc.wantIssues) 68 | case len(issues) > 0: 69 | r.Empty(issues) 70 | } 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/rules/package_no_import_cycle.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | var _ core.Rule = (*PackageNoImportCycle)(nil) 8 | 9 | // PackageNoImportCycle this is an extra uncategorized rule that detects package import cycles. 10 | // The Protobuf compiler outlaws circular file imports, but it's still possible to introduce package cycles, such as these: 11 | type PackageNoImportCycle struct { 12 | // cache is a map of package name to a slice of package names that it imports 13 | cache map[string][]string 14 | } 15 | 16 | func (p *PackageNoImportCycle) lazyInit() { 17 | if p.cache == nil { 18 | p.cache = make(map[string][]string) 19 | } 20 | } 21 | 22 | // Message implements lint.Rule. 23 | func (p *PackageNoImportCycle) Message() string { 24 | return "package should not have import cycles" 25 | } 26 | 27 | // Validate implements lint.Rule. 28 | func (p *PackageNoImportCycle) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 29 | p.lazyInit() 30 | panic("implement me") 31 | } 32 | -------------------------------------------------------------------------------- /internal/rules/package_same_csharp_namespace.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | var _ core.Rule = (*PackageSameCsharpNamespace)(nil) 8 | 9 | // PackageSameCsharpNamespace checks that all files with a given package have the same value for the csharp_namespace option. 10 | type PackageSameCsharpNamespace struct { 11 | // dir => package 12 | cache map[string]string 13 | } 14 | 15 | func (p *PackageSameCsharpNamespace) lazyInit() { 16 | if p.cache == nil { 17 | p.cache = make(map[string]string) 18 | } 19 | } 20 | 21 | // Message implements lint.Rule. 22 | func (p *PackageSameCsharpNamespace) Message() string { 23 | return "different proto files in the same package should have the same csharp_namespace" 24 | } 25 | 26 | // Validate implements lint.Rule. 27 | func (p *PackageSameCsharpNamespace) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 28 | p.lazyInit() 29 | 30 | var res []core.Issue 31 | 32 | if len(protoInfo.Info.ProtoBody.Packages) == 0 { 33 | return res, nil 34 | } 35 | 36 | packageName := protoInfo.Info.ProtoBody.Packages[0].Name 37 | for _, option := range protoInfo.Info.ProtoBody.Options { 38 | if option.OptionName == "csharp_namespace" { 39 | if p.cache[packageName] == "" { 40 | p.cache[packageName] = option.Constant 41 | continue 42 | } 43 | 44 | if p.cache[packageName] != option.Constant { 45 | res = core.AppendIssue(res, p, option.Meta.Pos, option.Constant, option.Comments) 46 | } 47 | } 48 | } 49 | 50 | return res, nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/rules/package_same_csharp_namespace_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestPackageSameCSharpNamespace_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "different proto files in the same package should have the same csharp_namespace" 19 | 20 | rule := rules.PackageSameCsharpNamespace{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestPackageSameCSharpNamespace_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileNames []string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileNames: []string{invalidAuthProto5, invalidAuthProto6}, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 142, 40 | Line: 9, 41 | Column: 1, 42 | }, 43 | SourceName: `"ZergsLaw.BackTemplate.Api.Session.V2"`, 44 | Message: "different proto files in the same package should have the same csharp_namespace", 45 | RuleName: "PACKAGE_SAME_CSHARP_NAMESPACE", 46 | }, 47 | wantErr: nil, 48 | }, 49 | "valid": { 50 | fileNames: []string{validAuthProto, validAuthProto2}, 51 | wantErr: nil, 52 | }, 53 | } 54 | 55 | for name, tc := range tests { 56 | name, tc := name, tc 57 | t.Run(name, func(t *testing.T) { 58 | t.Parallel() 59 | 60 | r, protos := start(t) 61 | 62 | rule := rules.PackageSameCsharpNamespace{} 63 | var issues []core.Issue 64 | for _, fileName := range tc.fileNames { 65 | issue, err := rule.Validate(protos[fileName]) 66 | r.ErrorIs(err, tc.wantErr) 67 | if err == nil { 68 | issues = append(issues, issue...) 69 | } 70 | } 71 | 72 | switch { 73 | case tc.wantIssues != nil: 74 | r.Contains(issues, *tc.wantIssues) 75 | case len(issues) > 0: 76 | r.Empty(issues) 77 | } 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /internal/rules/package_same_directory.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/easyp-tech/easyp/internal/core" 7 | ) 8 | 9 | var _ core.Rule = (*PackageSameDirectory)(nil) 10 | 11 | // PackageSameDirectory this rule checks that all files with a given package are in the same directory. 12 | type PackageSameDirectory struct { 13 | // dir => package 14 | cache map[string]string 15 | } 16 | 17 | func (d *PackageSameDirectory) lazyInit() { 18 | if d.cache == nil { 19 | d.cache = make(map[string]string) 20 | } 21 | } 22 | 23 | // Message implements lint.Rule. 24 | func (d *PackageSameDirectory) Message() string { 25 | return "different proto files in the same package should be in the same directory" 26 | } 27 | 28 | // Validate implements lint.Rule. 29 | func (d *PackageSameDirectory) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 30 | d.lazyInit() 31 | 32 | var res []core.Issue 33 | 34 | directory := filepath.Dir(protoInfo.Path) 35 | for _, packageInfo := range protoInfo.Info.ProtoBody.Packages { 36 | if d.cache[packageInfo.Name] == "" { 37 | d.cache[packageInfo.Name] = directory 38 | continue 39 | } 40 | 41 | if d.cache[packageInfo.Name] != directory { 42 | res = core.AppendIssue(res, d, packageInfo.Meta.Pos, packageInfo.Name, packageInfo.Comments) 43 | } 44 | } 45 | 46 | return res, nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/rules/package_same_directory_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 9 | 10 | "github.com/easyp-tech/easyp/internal/core" 11 | "github.com/easyp-tech/easyp/internal/rules" 12 | ) 13 | 14 | func TestPackageSameDirectory_Message(t *testing.T) { 15 | t.Parallel() 16 | 17 | assert := require.New(t) 18 | 19 | const expMessage = "different proto files in the same package should be in the same directory" 20 | 21 | rule := rules.PackageSameDirectory{} 22 | message := rule.Message() 23 | 24 | assert.Equal(expMessage, message) 25 | } 26 | 27 | func TestPackageSameDirectory_Validate(t *testing.T) { 28 | t.Parallel() 29 | 30 | tests := map[string]struct { 31 | fileNames []string 32 | wantIssues *core.Issue 33 | wantErr error 34 | }{ 35 | "invalid": { 36 | fileNames: []string{invalidAuthProto2, invalidAuthProto4}, 37 | wantIssues: &core.Issue{ 38 | Position: meta.Position{ 39 | Filename: "", 40 | Offset: 0, 41 | Line: 0, 42 | Column: 0, 43 | }, 44 | SourceName: "", 45 | Message: "", 46 | RuleName: "PACKAGE_SAME_DIRECTORY", 47 | }, 48 | wantErr: nil, 49 | }, 50 | "valid": { 51 | fileNames: []string{validAuthProto, validAuthProto2}, 52 | wantErr: nil, 53 | }, 54 | } 55 | 56 | for name, tc := range tests { 57 | name, tc := name, tc 58 | t.Run(name, func(t *testing.T) { 59 | t.Parallel() 60 | 61 | r, protos := start(t) 62 | 63 | rule := rules.PackageSameDirectory{} 64 | var got []error 65 | for _, fileName := range tc.fileNames { 66 | _, err := rule.Validate(protos[fileName]) 67 | got = append(got, err) 68 | } 69 | 70 | r.ErrorIs(errors.Join(got...), tc.wantErr) 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/rules/package_same_go_package.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | var _ core.Rule = (*PackageSameGoPackage)(nil) 8 | 9 | // PackageSameGoPackage checks that all files with a given package have the same value for the go_package option. 10 | type PackageSameGoPackage struct { 11 | // dir => package 12 | cache map[string]string 13 | } 14 | 15 | func (p *PackageSameGoPackage) lazyInit() { 16 | if p.cache == nil { 17 | p.cache = make(map[string]string) 18 | } 19 | } 20 | 21 | // Message implements lint.Rule. 22 | func (p *PackageSameGoPackage) Message() string { 23 | return "all files in the same package must have the same go_package name" 24 | } 25 | 26 | // Validate implements lint.Rule. 27 | func (p *PackageSameGoPackage) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 28 | p.lazyInit() 29 | 30 | var res []core.Issue 31 | 32 | if len(protoInfo.Info.ProtoBody.Packages) == 0 { 33 | return res, nil 34 | } 35 | 36 | packageName := protoInfo.Info.ProtoBody.Packages[0].Name 37 | for _, option := range protoInfo.Info.ProtoBody.Options { 38 | if option.OptionName == "go_package" { 39 | if p.cache[packageName] == "" { 40 | p.cache[packageName] = option.Constant 41 | continue 42 | } 43 | 44 | if p.cache[packageName] != option.Constant { 45 | res = core.AppendIssue(res, p, option.Meta.Pos, option.Constant, option.Comments) 46 | } 47 | } 48 | } 49 | 50 | return res, nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/rules/package_same_go_package_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 9 | 10 | "github.com/easyp-tech/easyp/internal/core" 11 | "github.com/easyp-tech/easyp/internal/rules" 12 | ) 13 | 14 | func TestPackageSameGoPackage_Message(t *testing.T) { 15 | t.Parallel() 16 | 17 | assert := require.New(t) 18 | 19 | const expMessage = "all files in the same package must have the same go_package name" 20 | 21 | rule := rules.PackageSameGoPackage{} 22 | message := rule.Message() 23 | 24 | assert.Equal(expMessage, message) 25 | } 26 | 27 | func TestPackageSameGoPackage_Validate(t *testing.T) { 28 | t.Parallel() 29 | 30 | tests := map[string]struct { 31 | fileNames []string 32 | wantIssues *core.Issue 33 | wantErr error 34 | }{ 35 | "valid": { 36 | fileNames: []string{invalidAuthProto5, invalidAuthProto6}, 37 | wantErr: nil, 38 | }, 39 | "invalid": { 40 | fileNames: []string{validAuthProto, validAuthProto2}, 41 | wantIssues: &core.Issue{ 42 | Position: meta.Position{ 43 | Filename: "", 44 | Offset: 0, 45 | Line: 0, 46 | Column: 0, 47 | }, 48 | SourceName: "", 49 | Message: "", 50 | RuleName: "PACKAGE_SAME_GO_PACKAGE", 51 | }, 52 | wantErr: nil, 53 | }, 54 | } 55 | 56 | for name, tc := range tests { 57 | name, tc := name, tc 58 | t.Run(name, func(t *testing.T) { 59 | t.Parallel() 60 | 61 | r, protos := start(t) 62 | 63 | rule := rules.PackageSameGoPackage{} 64 | var got []error 65 | for _, fileName := range tc.fileNames { 66 | _, err := rule.Validate(protos[fileName]) 67 | got = append(got, err) 68 | } 69 | 70 | r.ErrorIs(errors.Join(got...), tc.wantErr) 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/rules/package_same_java_multiple_files.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | var _ core.Rule = (*PackageSameJavaMultipleFiles)(nil) 8 | 9 | // PackageSameJavaMultipleFiles checks that all files with a given package have the same value for the java_multiple_files option. 10 | type PackageSameJavaMultipleFiles struct { 11 | // dir => package 12 | cache map[string]string 13 | } 14 | 15 | func (p *PackageSameJavaMultipleFiles) lazyInit() { 16 | if p.cache == nil { 17 | p.cache = make(map[string]string) 18 | } 19 | } 20 | 21 | // Message implements lint.Rule. 22 | func (p *PackageSameJavaMultipleFiles) Message() string { 23 | return "all files in the same package must have the same java_multiple_files option" 24 | } 25 | 26 | // Validate implements lint.Rule. 27 | func (p *PackageSameJavaMultipleFiles) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 28 | p.lazyInit() 29 | 30 | var res []core.Issue 31 | 32 | if len(protoInfo.Info.ProtoBody.Packages) == 0 { 33 | return res, nil 34 | } 35 | 36 | packageName := protoInfo.Info.ProtoBody.Packages[0].Name 37 | for _, option := range protoInfo.Info.ProtoBody.Options { 38 | if option.OptionName == "java_multiple_files" { 39 | if p.cache[packageName] == "" { 40 | p.cache[packageName] = option.Constant 41 | continue 42 | } 43 | 44 | if p.cache[packageName] != option.Constant { 45 | res = core.AppendIssue(res, p, option.Meta.Pos, option.Constant, option.Comments) 46 | } 47 | } 48 | } 49 | 50 | return res, nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/rules/package_same_java_multiple_files_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 9 | 10 | "github.com/easyp-tech/easyp/internal/core" 11 | "github.com/easyp-tech/easyp/internal/rules" 12 | ) 13 | 14 | func TestPackageSameJavaMultipleFiles_Message(t *testing.T) { 15 | t.Parallel() 16 | 17 | assert := require.New(t) 18 | 19 | const expMessage = "all files in the same package must have the same java_multiple_files option" 20 | 21 | rule := rules.PackageSameJavaMultipleFiles{} 22 | message := rule.Message() 23 | 24 | assert.Equal(expMessage, message) 25 | } 26 | 27 | func TestPackageSameJavaMultipleFiles_Validate(t *testing.T) { 28 | t.Parallel() 29 | 30 | tests := map[string]struct { 31 | fileNames []string 32 | wantIssues *core.Issue 33 | wantErr error 34 | }{ 35 | "invalid": { 36 | fileNames: []string{invalidAuthProto5, invalidAuthProto6}, 37 | wantIssues: &core.Issue{ 38 | Position: meta.Position{ 39 | Filename: "", 40 | Offset: 0, 41 | Line: 0, 42 | Column: 0, 43 | }, 44 | SourceName: "", 45 | Message: "", 46 | RuleName: "PACKAGE_SAME_JAVA_MULTIPLE_FILES", 47 | }, 48 | }, 49 | "valid": { 50 | fileNames: []string{validAuthProto, validAuthProto2}, 51 | wantErr: nil, 52 | }, 53 | } 54 | 55 | for name, tc := range tests { 56 | name, tc := name, tc 57 | t.Run(name, func(t *testing.T) { 58 | t.Parallel() 59 | 60 | r, protos := start(t) 61 | 62 | rule := rules.PackageSameJavaMultipleFiles{} 63 | var got []error 64 | for _, fileName := range tc.fileNames { 65 | _, err := rule.Validate(protos[fileName]) 66 | got = append(got, err) 67 | } 68 | 69 | r.ErrorIs(errors.Join(got...), tc.wantErr) 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/rules/package_same_java_package.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | var _ core.Rule = (*PackageSameJavaPackage)(nil) 8 | 9 | // PackageSameJavaPackage checks that all files with a given package have the same value for the java_package option. 10 | type PackageSameJavaPackage struct { 11 | // dir => package 12 | cache map[string]string 13 | } 14 | 15 | func (p *PackageSameJavaPackage) lazyInit() { 16 | if p.cache == nil { 17 | p.cache = make(map[string]string) 18 | } 19 | } 20 | 21 | // Message implements lint.Rule. 22 | func (p *PackageSameJavaPackage) Message() string { 23 | return "all files in the same package must have the same java_package option" 24 | } 25 | 26 | // Validate implements lint.Rule. 27 | func (p *PackageSameJavaPackage) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 28 | p.lazyInit() 29 | 30 | var res []core.Issue 31 | 32 | if len(protoInfo.Info.ProtoBody.Packages) == 0 { 33 | return res, nil 34 | } 35 | 36 | packageName := protoInfo.Info.ProtoBody.Packages[0].Name 37 | for _, option := range protoInfo.Info.ProtoBody.Options { 38 | if option.OptionName == "java_package" { 39 | if p.cache[packageName] == "" { 40 | p.cache[packageName] = option.Constant 41 | continue 42 | } 43 | 44 | if p.cache[packageName] != option.Constant { 45 | res = core.AppendIssue(res, p, option.Meta.Pos, option.Constant, option.Comments) 46 | } 47 | } 48 | } 49 | 50 | return res, nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/rules/package_same_java_package_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 9 | 10 | "github.com/easyp-tech/easyp/internal/core" 11 | "github.com/easyp-tech/easyp/internal/rules" 12 | ) 13 | 14 | func TestPackageSameJavaPackage_Message(t *testing.T) { 15 | t.Parallel() 16 | 17 | assert := require.New(t) 18 | 19 | const expMessage = "all files in the same package must have the same java_package option" 20 | 21 | rule := rules.PackageSameJavaPackage{} 22 | message := rule.Message() 23 | 24 | assert.Equal(expMessage, message) 25 | } 26 | 27 | func TestPackageSameJavaPackage_Validate(t *testing.T) { 28 | t.Parallel() 29 | 30 | tests := map[string]struct { 31 | fileNames []string 32 | wantIssues *core.Issue 33 | wantErr error 34 | }{ 35 | "valid": { 36 | fileNames: []string{invalidAuthProto5, invalidAuthProto6}, 37 | wantIssues: &core.Issue{ 38 | Position: meta.Position{ 39 | Filename: "", 40 | Offset: 0, 41 | Line: 0, 42 | Column: 0, 43 | }, 44 | SourceName: "", 45 | Message: "", 46 | RuleName: "PACKAGE_SAME_JAVA_PACKAGE", 47 | }, 48 | }, 49 | "invalid": { 50 | fileNames: []string{validAuthProto, validAuthProto2}, 51 | wantErr: nil, 52 | }, 53 | } 54 | 55 | for name, tc := range tests { 56 | name, tc := name, tc 57 | t.Run(name, func(t *testing.T) { 58 | t.Parallel() 59 | 60 | r, protos := start(t) 61 | 62 | rule := rules.PackageSameJavaPackage{} 63 | var got []error 64 | for _, fileName := range tc.fileNames { 65 | _, err := rule.Validate(protos[fileName]) 66 | got = append(got, err) 67 | } 68 | 69 | r.ErrorIs(errors.Join(got...), tc.wantErr) 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/rules/package_same_php_namespace.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | var _ core.Rule = (*PackageSamePHPNamespace)(nil) 8 | 9 | // PackageSamePHPNamespace checks that all files with a given package have the same value for the php_namespace option. 10 | type PackageSamePHPNamespace struct { 11 | // dir => package 12 | cache map[string]string 13 | } 14 | 15 | func (p *PackageSamePHPNamespace) lazyInit() { 16 | if p.cache == nil { 17 | p.cache = make(map[string]string) 18 | } 19 | } 20 | 21 | // Message implements lint.Rule. 22 | func (p *PackageSamePHPNamespace) Message() string { 23 | return "all files in the same package must have the same php_namespace option" 24 | } 25 | 26 | // Validate implements lint.Rule. 27 | func (p *PackageSamePHPNamespace) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 28 | p.lazyInit() 29 | 30 | var res []core.Issue 31 | 32 | if len(protoInfo.Info.ProtoBody.Packages) == 0 { 33 | return res, nil 34 | } 35 | 36 | packageName := protoInfo.Info.ProtoBody.Packages[0].Name 37 | for _, option := range protoInfo.Info.ProtoBody.Options { 38 | if option.OptionName == "php_namespace" { 39 | if p.cache[packageName] == "" { 40 | p.cache[packageName] = option.Constant 41 | continue 42 | } 43 | 44 | if p.cache[packageName] != option.Constant { 45 | res = core.AppendIssue(res, p, option.Meta.Pos, option.Constant, option.Comments) 46 | } 47 | } 48 | } 49 | 50 | return res, nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/rules/package_same_php_namespace_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 9 | 10 | "github.com/easyp-tech/easyp/internal/core" 11 | "github.com/easyp-tech/easyp/internal/rules" 12 | ) 13 | 14 | func TestPackageSamePHPNamespace_Message(t *testing.T) { 15 | t.Parallel() 16 | 17 | assert := require.New(t) 18 | 19 | const expMessage = "all files in the same package must have the same php_namespace option" 20 | 21 | rule := rules.PackageSamePHPNamespace{} 22 | message := rule.Message() 23 | 24 | assert.Equal(expMessage, message) 25 | } 26 | 27 | func TestPackageSamePHPNamespace_Validate(t *testing.T) { 28 | t.Parallel() 29 | 30 | tests := map[string]struct { 31 | fileNames []string 32 | wantIssues *core.Issue 33 | wantErr error 34 | }{ 35 | "invalid": { 36 | fileNames: []string{invalidAuthProto5, invalidAuthProto6}, 37 | wantIssues: &core.Issue{ 38 | Position: meta.Position{ 39 | Filename: "", 40 | Offset: 0, 41 | Line: 0, 42 | Column: 0, 43 | }, 44 | SourceName: "", 45 | Message: "", 46 | RuleName: "PACKAGE_SAME_PHP_NAMESPACE", 47 | }, 48 | }, 49 | "valid": { 50 | fileNames: []string{validAuthProto, validAuthProto2}, 51 | wantErr: nil, 52 | }, 53 | } 54 | 55 | for name, tc := range tests { 56 | name, tc := name, tc 57 | t.Run(name, func(t *testing.T) { 58 | t.Parallel() 59 | 60 | r, protos := start(t) 61 | 62 | rule := rules.PackageSamePHPNamespace{} 63 | var got []error 64 | for _, fileName := range tc.fileNames { 65 | _, err := rule.Validate(protos[fileName]) 66 | got = append(got, err) 67 | } 68 | 69 | r.ErrorIs(errors.Join(got...), tc.wantErr) 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/rules/package_same_ruby_package.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | var _ core.Rule = (*PackageSameRubyPackage)(nil) 8 | 9 | // PackageSameRubyPackage checks that all files with a given package have the same value for the ruby_package option. 10 | type PackageSameRubyPackage struct { 11 | // dir => package 12 | cache map[string]string 13 | } 14 | 15 | func (p *PackageSameRubyPackage) lazyInit() { 16 | if p.cache == nil { 17 | p.cache = make(map[string]string) 18 | } 19 | } 20 | 21 | // Message implements lint.Rule. 22 | func (p *PackageSameRubyPackage) Message() string { 23 | return "all files in the same package must have the same ruby_package option" 24 | } 25 | 26 | // Validate implements lint.Rule. 27 | func (p *PackageSameRubyPackage) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 28 | p.lazyInit() 29 | 30 | var res []core.Issue 31 | 32 | if len(protoInfo.Info.ProtoBody.Packages) == 0 { 33 | return res, nil 34 | } 35 | 36 | packageName := protoInfo.Info.ProtoBody.Packages[0].Name 37 | for _, option := range protoInfo.Info.ProtoBody.Options { 38 | if option.OptionName == "ruby_package" { 39 | if p.cache[packageName] == "" { 40 | p.cache[packageName] = option.Constant 41 | continue 42 | } 43 | 44 | if p.cache[packageName] != option.Constant { 45 | res = core.AppendIssue(res, p, option.Meta.Pos, option.Constant, option.Comments) 46 | } 47 | } 48 | } 49 | 50 | return res, nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/rules/package_same_ruby_package_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 9 | 10 | "github.com/easyp-tech/easyp/internal/core" 11 | "github.com/easyp-tech/easyp/internal/rules" 12 | ) 13 | 14 | func TestPackageSameRubyPackage_Message(t *testing.T) { 15 | t.Parallel() 16 | 17 | assert := require.New(t) 18 | 19 | const expMessage = "all files in the same package must have the same ruby_package option" 20 | 21 | rule := rules.PackageSameRubyPackage{} 22 | message := rule.Message() 23 | 24 | assert.Equal(expMessage, message) 25 | } 26 | 27 | func TestPackageSameRubyPackage_Validate(t *testing.T) { 28 | t.Parallel() 29 | 30 | tests := map[string]struct { 31 | fileNames []string 32 | wantIssues *core.Issue 33 | wantErr error 34 | }{ 35 | "valid": { 36 | fileNames: []string{invalidAuthProto5, invalidAuthProto6}, 37 | wantIssues: &core.Issue{ 38 | Position: meta.Position{ 39 | Filename: "", 40 | Offset: 0, 41 | Line: 0, 42 | Column: 0, 43 | }, 44 | SourceName: "", 45 | Message: "", 46 | RuleName: "PACKAGE_SAME_RUBY_PACKAGE", 47 | }, 48 | }, 49 | "invalid": { 50 | fileNames: []string{validAuthProto, validAuthProto2}, 51 | wantErr: nil, 52 | }, 53 | } 54 | 55 | for name, tc := range tests { 56 | name, tc := name, tc 57 | t.Run(name, func(t *testing.T) { 58 | t.Parallel() 59 | 60 | r, protos := start(t) 61 | 62 | rule := rules.PackageSameRubyPackage{} 63 | var got []error 64 | for _, fileName := range tc.fileNames { 65 | _, err := rule.Validate(protos[fileName]) 66 | got = append(got, err) 67 | } 68 | 69 | r.ErrorIs(errors.Join(got...), tc.wantErr) 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/rules/package_same_swift_prefix.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | var _ core.Rule = (*PackageSameSwiftPrefix)(nil) 8 | 9 | // PackageSameSwiftPrefix checks that all files with a given package have the same value for the swift_prefix option. 10 | type PackageSameSwiftPrefix struct { 11 | // dir => package 12 | cache map[string]string 13 | } 14 | 15 | func (p *PackageSameSwiftPrefix) lazyInit() { 16 | if p.cache == nil { 17 | p.cache = make(map[string]string) 18 | } 19 | } 20 | 21 | // Message implements lint.Rule. 22 | func (p *PackageSameSwiftPrefix) Message() string { 23 | return "all files in the same package must have the same swift_prefix option" 24 | } 25 | 26 | // Validate implements lint.Rule. 27 | func (p *PackageSameSwiftPrefix) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 28 | p.lazyInit() 29 | 30 | var res []core.Issue 31 | 32 | if len(protoInfo.Info.ProtoBody.Packages) == 0 { 33 | return res, nil 34 | } 35 | 36 | packageName := protoInfo.Info.ProtoBody.Packages[0].Name 37 | for _, option := range protoInfo.Info.ProtoBody.Options { 38 | if option.OptionName == "ruby_package" { 39 | if p.cache[packageName] == "" { 40 | p.cache[packageName] = option.Constant 41 | continue 42 | } 43 | 44 | if p.cache[packageName] != option.Constant { 45 | res = core.AppendIssue(res, p, option.Meta.Pos, option.Constant, option.Comments) 46 | } 47 | } 48 | } 49 | 50 | return res, nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/rules/package_same_swift_prefix_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 9 | 10 | "github.com/easyp-tech/easyp/internal/core" 11 | "github.com/easyp-tech/easyp/internal/rules" 12 | ) 13 | 14 | func TestPackageSameSwiftPrefix_Message(t *testing.T) { 15 | t.Parallel() 16 | 17 | assert := require.New(t) 18 | 19 | const expMessage = "all files in the same package must have the same swift_prefix option" 20 | 21 | rule := rules.PackageSameSwiftPrefix{} 22 | message := rule.Message() 23 | 24 | assert.Equal(expMessage, message) 25 | } 26 | 27 | func TestPackageSameSwiftPrefix_Validate(t *testing.T) { 28 | t.Parallel() 29 | 30 | tests := map[string]struct { 31 | fileNames []string 32 | wantIssues *core.Issue 33 | wantErr error 34 | }{ 35 | "valid": { 36 | fileNames: []string{invalidAuthProto5, invalidAuthProto6}, 37 | wantIssues: &core.Issue{ 38 | Position: meta.Position{ 39 | Filename: "", 40 | Offset: 0, 41 | Line: 0, 42 | Column: 0, 43 | }, 44 | SourceName: "", 45 | Message: "", 46 | RuleName: "PACKAGE_SAME_SWIFT_PREFIX", 47 | }, 48 | }, 49 | "invalid": { 50 | fileNames: []string{validAuthProto, validAuthProto2}, 51 | wantErr: nil, 52 | }, 53 | } 54 | 55 | for name, tc := range tests { 56 | name, tc := name, tc 57 | t.Run(name, func(t *testing.T) { 58 | t.Parallel() 59 | 60 | r, protos := start(t) 61 | 62 | rule := rules.PackageSameSwiftPrefix{} 63 | var got []error 64 | for _, fileName := range tc.fileNames { 65 | _, err := rule.Validate(protos[fileName]) 66 | got = append(got, err) 67 | } 68 | 69 | r.ErrorIs(errors.Join(got...), tc.wantErr) 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/rules/package_version_suffix.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/easyp-tech/easyp/internal/core" 7 | ) 8 | 9 | var _ core.Rule = (*PackageVersionSuffix)(nil) 10 | 11 | // PackageVersionSuffix this rule enforces that the last component of a package must be a version of the form 12 | // v\d+, v\d+test.*, v\d+(alpha|beta)\d*, or v\d+p\d+(alpha|beta)\d*, where numbers are >=1. 13 | type PackageVersionSuffix struct{} 14 | 15 | // Message implements lint.Rule. 16 | func (p *PackageVersionSuffix) Message() string { 17 | return "package name should have a version suffix" 18 | } 19 | 20 | var matchVersionSuffix = regexp.MustCompile(`.*v\d+|.*v\d+test.*|.*v\d+(alpha|beta)\d*|.*v\d+p\d+(alpha|beta)\d*$`) 21 | 22 | // Validate implements lint.Rule. 23 | func (p *PackageVersionSuffix) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 24 | var res []core.Issue 25 | 26 | for _, pkg := range protoInfo.Info.ProtoBody.Packages { 27 | if !matchVersionSuffix.MatchString(pkg.Name) { 28 | res = core.AppendIssue(res, p, pkg.Meta.Pos, pkg.Name, pkg.Comments) 29 | } 30 | } 31 | 32 | return res, nil 33 | } 34 | -------------------------------------------------------------------------------- /internal/rules/package_version_suffix_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestPackageVersionSuffix_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "package name should have a version suffix" 19 | 20 | rule := rules.PackageVersionSuffix{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestPackageVersionSuffix_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 20, 40 | Line: 3, 41 | Column: 1, 42 | }, 43 | SourceName: "Session", 44 | Message: "package name should have a version suffix", 45 | RuleName: "PACKAGE_VERSION_SUFFIX", 46 | }, 47 | }, 48 | "valid": { 49 | fileName: validAuthProto, 50 | wantErr: nil, 51 | }, 52 | } 53 | 54 | for name, tc := range tests { 55 | name, tc := name, tc 56 | t.Run(name, func(t *testing.T) { 57 | t.Parallel() 58 | 59 | r, protos := start(t) 60 | 61 | rule := rules.PackageVersionSuffix{} 62 | issues, err := rule.Validate(protos[tc.fileName]) 63 | r.ErrorIs(err, tc.wantErr) 64 | switch { 65 | case tc.wantIssues != nil: 66 | r.Contains(issues, *tc.wantIssues) 67 | case len(issues) > 0: 68 | r.Empty(issues) 69 | } 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/rules/rpc_no_client_streaming.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | var _ core.Rule = (*RPCNoClientStreaming)(nil) 8 | 9 | // RPCNoClientStreaming this rule checks that RPCs aren't client streaming. 10 | type RPCNoClientStreaming struct { 11 | } 12 | 13 | // Message implements lint.Rule. 14 | func (r *RPCNoClientStreaming) Message() string { 15 | return "client streaming RPCs are not allowed" 16 | } 17 | 18 | // Validate implements lint.Rule. 19 | func (r *RPCNoClientStreaming) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 20 | var res []core.Issue 21 | 22 | for _, service := range protoInfo.Info.ProtoBody.Services { 23 | for _, rpc := range service.ServiceBody.RPCs { 24 | if rpc.RPCRequest.IsStream { 25 | res = core.AppendIssue(res, r, rpc.Meta.Pos, rpc.RPCName, rpc.Comments) 26 | } 27 | } 28 | } 29 | 30 | return res, nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/rules/rpc_no_client_streaming_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestRPCNoClientStreaming_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "client streaming RPCs are not allowed" 19 | 20 | rule := rules.RPCNoClientStreaming{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestRPCNoClientStreaming_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 300, 40 | Line: 13, 41 | Column: 3, 42 | }, 43 | SourceName: "delete", 44 | Message: "client streaming RPCs are not allowed", 45 | RuleName: "RPC_NO_CLIENT_STREAMING", 46 | }, 47 | }, 48 | "valid": { 49 | fileName: validAuthProto, 50 | wantErr: nil, 51 | }, 52 | } 53 | 54 | for name, tc := range tests { 55 | name, tc := name, tc 56 | t.Run(name, func(t *testing.T) { 57 | t.Parallel() 58 | 59 | r, protos := start(t) 60 | 61 | rule := rules.RPCNoClientStreaming{} 62 | issues, err := rule.Validate(protos[tc.fileName]) 63 | r.ErrorIs(err, tc.wantErr) 64 | switch { 65 | case tc.wantIssues != nil: 66 | r.Contains(issues, *tc.wantIssues) 67 | case len(issues) > 0: 68 | r.Empty(issues) 69 | } 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/rules/rpc_no_server_streaming.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | var _ core.Rule = (*RPCNoServerStreaming)(nil) 8 | 9 | // RPCNoServerStreaming this rule checks that RPCs aren't server streaming. 10 | type RPCNoServerStreaming struct { 11 | } 12 | 13 | // Message implements lint.Rule. 14 | func (r *RPCNoServerStreaming) Message() string { 15 | return "server streaming RPCs are not allowed" 16 | } 17 | 18 | // Validate implements lint.Rule. 19 | func (r *RPCNoServerStreaming) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 20 | var res []core.Issue 21 | 22 | for _, service := range protoInfo.Info.ProtoBody.Services { 23 | for _, rpc := range service.ServiceBody.RPCs { 24 | if rpc.RPCResponse.IsStream { 25 | res = core.AppendIssue(res, r, rpc.Meta.Pos, rpc.RPCName, rpc.Comments) 26 | } 27 | } 28 | } 29 | 30 | return res, nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/rules/rpc_no_server_streaming_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestRPCNoServerStreaming_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "server streaming RPCs are not allowed" 19 | 20 | rule := rules.RPCNoServerStreaming{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestRPCNoServerStreaming_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 300, 40 | Line: 13, 41 | Column: 3, 42 | }, 43 | SourceName: "delete", 44 | Message: "server streaming RPCs are not allowed", 45 | RuleName: "RPC_NO_SERVER_STREAMING", 46 | }, 47 | }, 48 | "valid": { 49 | fileName: validAuthProto, 50 | wantErr: nil, 51 | }, 52 | } 53 | 54 | for name, tc := range tests { 55 | name, tc := name, tc 56 | t.Run(name, func(t *testing.T) { 57 | t.Parallel() 58 | 59 | r, protos := start(t) 60 | 61 | rule := rules.RPCNoServerStreaming{} 62 | issues, err := rule.Validate(protos[tc.fileName]) 63 | r.ErrorIs(err, tc.wantErr) 64 | switch { 65 | case tc.wantIssues != nil: 66 | r.Contains(issues, *tc.wantIssues) 67 | case len(issues) > 0: 68 | r.Empty(issues) 69 | } 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/rules/rpc_pascal_case.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/easyp-tech/easyp/internal/core" 7 | ) 8 | 9 | var _ core.Rule = (*RPCPascalCase)(nil) 10 | 11 | // RPCPascalCase this rule checks that RPCs are PascalCase. 12 | type RPCPascalCase struct{} 13 | 14 | // Message implements lint.Rule. 15 | func (c *RPCPascalCase) Message() string { 16 | return "RPC names should be PascalCase" 17 | } 18 | 19 | // Validate implements lint.Rule. 20 | func (c *RPCPascalCase) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 21 | var res []core.Issue 22 | pascalCase := regexp.MustCompile("^[A-Z][a-zA-Z0-9]*$") 23 | for _, service := range protoInfo.Info.ProtoBody.Services { 24 | for _, rpc := range service.ServiceBody.RPCs { 25 | if !pascalCase.MatchString(rpc.RPCName) { 26 | res = core.AppendIssue(res, c, rpc.Meta.Pos, rpc.RPCName, rpc.Comments) 27 | } 28 | } 29 | } 30 | 31 | return res, nil 32 | } 33 | -------------------------------------------------------------------------------- /internal/rules/rpc_pascal_case_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestRPCPascalCase_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "RPC names should be PascalCase" 19 | 20 | rule := rules.RPCPascalCase{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestRPCPascalCase_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 300, 40 | Line: 13, 41 | Column: 3, 42 | }, 43 | SourceName: "delete", 44 | Message: "RPC names should be PascalCase", 45 | RuleName: "RPC_PASCAL_CASE", 46 | }, 47 | }, 48 | "valid": { 49 | fileName: validAuthProto, 50 | wantErr: nil, 51 | }, 52 | } 53 | 54 | for name, tc := range tests { 55 | name, tc := name, tc 56 | t.Run(name, func(t *testing.T) { 57 | t.Parallel() 58 | 59 | r, protos := start(t) 60 | 61 | rule := rules.RPCPascalCase{} 62 | issues, err := rule.Validate(protos[tc.fileName]) 63 | r.ErrorIs(err, tc.wantErr) 64 | switch { 65 | case tc.wantIssues != nil: 66 | r.Contains(issues, *tc.wantIssues) 67 | case len(issues) > 0: 68 | r.Empty(issues) 69 | } 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/rules/rpc_request_response_unique.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/samber/lo" 5 | 6 | "github.com/easyp-tech/easyp/internal/core" 7 | ) 8 | 9 | var _ core.Rule = (*RPCRequestResponseUnique)(nil) 10 | 11 | // RPCRequestResponseUnique checks that RPCs request and response types are only used in one RPC. 12 | type RPCRequestResponseUnique struct { 13 | } 14 | 15 | // Message implements lint.Rule. 16 | func (r *RPCRequestResponseUnique) Message() string { 17 | return "request and response types must be unique across all RPCs" 18 | } 19 | 20 | // Validate implements lint.Rule. 21 | func (r *RPCRequestResponseUnique) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 22 | var res []core.Issue 23 | var messages []string 24 | 25 | for _, service := range protoInfo.Info.ProtoBody.Services { 26 | for _, rpc := range service.ServiceBody.RPCs { 27 | if !lo.Contains(messages, rpc.RPCRequest.MessageType) { 28 | messages = append(messages, rpc.RPCRequest.MessageType) 29 | } else { 30 | res = core.AppendIssue(res, r, rpc.Meta.Pos, rpc.RPCRequest.MessageType, rpc.Comments) 31 | } 32 | if !lo.Contains(messages, rpc.RPCResponse.MessageType) { 33 | messages = append(messages, rpc.RPCResponse.MessageType) 34 | } else { 35 | res = core.AppendIssue(res, r, rpc.Meta.Pos, rpc.RPCResponse.MessageType, rpc.Comments) 36 | } 37 | } 38 | } 39 | 40 | return res, nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/rules/rpc_request_response_unique_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestRPCRequestResponseUnique_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "request and response types must be unique across all RPCs" 19 | 20 | rule := rules.RPCRequestResponseUnique{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestRPCRequestResponseUnique_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 375, 40 | Line: 14, 41 | Column: 3, 42 | }, 43 | SourceName: "TokenData", 44 | Message: "request and response types must be unique across all RPCs", 45 | RuleName: "RPC_REQUEST_RESPONSE_UNIQUE", 46 | }, 47 | }, 48 | "valid": { 49 | fileName: validAuthProto, 50 | wantErr: nil, 51 | }, 52 | } 53 | 54 | for name, tc := range tests { 55 | name, tc := name, tc 56 | t.Run(name, func(t *testing.T) { 57 | t.Parallel() 58 | 59 | r, protos := start(t) 60 | 61 | rule := rules.RPCRequestResponseUnique{} 62 | issues, err := rule.Validate(protos[tc.fileName]) 63 | r.ErrorIs(err, tc.wantErr) 64 | switch { 65 | case tc.wantIssues != nil: 66 | r.Contains(issues, *tc.wantIssues) 67 | case len(issues) > 0: 68 | r.Empty(issues) 69 | } 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/rules/rpc_request_standard_name.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | var _ core.Rule = (*RPCRequestStandardName)(nil) 8 | 9 | // RPCRequestStandardName checks that RPC request type names are RPCNameRequest or ServiceNameRPCNameRequest. 10 | type RPCRequestStandardName struct { 11 | } 12 | 13 | // Message implements lint.Rule. 14 | func (r *RPCRequestStandardName) Message() string { 15 | return "rpc request should have suffix 'Request'" 16 | } 17 | 18 | // Validate implements lint.Rule. 19 | func (r *RPCRequestStandardName) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 20 | var res []core.Issue 21 | 22 | for _, service := range protoInfo.Info.ProtoBody.Services { 23 | for _, rpc := range service.ServiceBody.RPCs { 24 | if rpc.RPCRequest.MessageType != rpc.RPCName+"Request" && rpc.RPCRequest.MessageType != service.ServiceName+rpc.RPCName+"Request" { 25 | res = core.AppendIssue(res, r, rpc.Meta.Pos, rpc.RPCRequest.MessageType, rpc.Comments) 26 | } 27 | } 28 | } 29 | 30 | return res, nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/rules/rpc_request_standard_name_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestRPCRequestStandardName_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "rpc request should have suffix 'Request'" 19 | 20 | rule := rules.RPCRequestStandardName{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestRPCRequestStandardName_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 214, 40 | Line: 11, 41 | Column: 3, 42 | }, 43 | SourceName: "SessionInfo", 44 | Message: "rpc request should have suffix 'Request'", 45 | RuleName: "RPC_REQUEST_STANDARD_NAME", 46 | }, 47 | }, 48 | "valid": { 49 | fileName: validAuthProto, 50 | wantErr: nil, 51 | }, 52 | } 53 | 54 | for name, tc := range tests { 55 | name, tc := name, tc 56 | t.Run(name, func(t *testing.T) { 57 | t.Parallel() 58 | 59 | r, protos := start(t) 60 | 61 | rule := rules.RPCRequestStandardName{} 62 | issues, err := rule.Validate(protos[tc.fileName]) 63 | r.ErrorIs(err, tc.wantErr) 64 | switch { 65 | case tc.wantIssues != nil: 66 | r.Contains(issues, *tc.wantIssues) 67 | case len(issues) > 0: 68 | r.Empty(issues) 69 | } 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/rules/rpc_response_standard_name.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "github.com/easyp-tech/easyp/internal/core" 5 | ) 6 | 7 | var _ core.Rule = (*RPCResponseStandardName)(nil) 8 | 9 | // RPCResponseStandardName checks that RPC response type names are RPCNameResponse or ServiceNameRPCNameResponse. 10 | type RPCResponseStandardName struct { 11 | } 12 | 13 | // Message implements lint.Rule. 14 | func (r *RPCResponseStandardName) Message() string { 15 | return "rpc response should have suffix 'Response'" 16 | } 17 | 18 | // Validate implements lint.Rule. 19 | func (r *RPCResponseStandardName) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 20 | var res []core.Issue 21 | 22 | for _, service := range protoInfo.Info.ProtoBody.Services { 23 | for _, rpc := range service.ServiceBody.RPCs { 24 | if rpc.RPCResponse.MessageType != rpc.RPCName+"Response" && rpc.RPCResponse.MessageType != service.ServiceName+rpc.RPCName+"Response" { 25 | res = core.AppendIssue(res, r, rpc.Meta.Pos, rpc.RPCResponse.MessageType, rpc.Comments) 26 | } 27 | } 28 | } 29 | 30 | return res, nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/rules/rpc_response_standard_name_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestRPCResponseStandardName_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "rpc response should have suffix 'Response'" 19 | 20 | rule := rules.RPCResponseStandardName{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestRPCResponseStandardName_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 214, 40 | Line: 11, 41 | Column: 3, 42 | }, 43 | SourceName: "Result", 44 | Message: "rpc response should have suffix 'Response'", 45 | RuleName: "RPC_RESPONSE_STANDARD_NAME", 46 | }, 47 | }, 48 | "valid": { 49 | fileName: validAuthProto, 50 | wantErr: nil, 51 | }, 52 | } 53 | 54 | for name, tc := range tests { 55 | name, tc := name, tc 56 | t.Run(name, func(t *testing.T) { 57 | t.Parallel() 58 | 59 | r, protos := start(t) 60 | 61 | rule := rules.RPCResponseStandardName{} 62 | issues, err := rule.Validate(protos[tc.fileName]) 63 | r.ErrorIs(err, tc.wantErr) 64 | switch { 65 | case tc.wantIssues != nil: 66 | r.Contains(issues, *tc.wantIssues) 67 | case len(issues) > 0: 68 | r.Empty(issues) 69 | } 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/rules/service_pascal_case.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/easyp-tech/easyp/internal/core" 7 | ) 8 | 9 | var _ core.Rule = (*ServicePascalCase)(nil) 10 | 11 | // ServicePascalCase this rule checks that services are PascalCase. 12 | type ServicePascalCase struct{} 13 | 14 | // Message implements lint.Rule. 15 | func (c *ServicePascalCase) Message() string { 16 | return "service names must be PascalCase" 17 | } 18 | 19 | // Validate implements lint.Rule. 20 | func (c *ServicePascalCase) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 21 | var res []core.Issue 22 | 23 | pascalCase := regexp.MustCompile("^[A-Z][a-z]+([A-Z]|[a-z]+)*$") 24 | for _, service := range protoInfo.Info.ProtoBody.Services { 25 | if !pascalCase.MatchString(service.ServiceName) { 26 | res = core.AppendIssue(res, c, service.Meta.Pos, service.ServiceName, service.Comments) 27 | } 28 | } 29 | 30 | return res, nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/rules/service_pascal_case_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestServicePascalCase_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "service names must be PascalCase" 19 | 20 | rule := rules.ServicePascalCase{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestServicePascalCase_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 197, 40 | Line: 10, 41 | Column: 1, 42 | }, 43 | SourceName: "auth", 44 | Message: "service names must be PascalCase", 45 | RuleName: "SERVICE_PASCAL_CASE", 46 | }, 47 | }, 48 | "valid": { 49 | fileName: validAuthProto, 50 | wantErr: nil, 51 | }, 52 | } 53 | 54 | for name, tc := range tests { 55 | name, tc := name, tc 56 | t.Run(name, func(t *testing.T) { 57 | t.Parallel() 58 | 59 | r, protos := start(t) 60 | 61 | rule := rules.ServicePascalCase{} 62 | issues, err := rule.Validate(protos[tc.fileName]) 63 | r.ErrorIs(err, tc.wantErr) 64 | switch { 65 | case tc.wantIssues != nil: 66 | r.Contains(issues, *tc.wantIssues) 67 | case len(issues) > 0: 68 | r.Empty(issues) 69 | } 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/rules/service_suffix.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/easyp-tech/easyp/internal/core" 7 | ) 8 | 9 | var _ core.Rule = (*ServiceSuffix)(nil) 10 | 11 | // ServiceSuffix this rule enforces that all services are suffixed with Service. 12 | type ServiceSuffix struct { 13 | Suffix string 14 | } 15 | 16 | // Message implements lint.Rule. 17 | func (s *ServiceSuffix) Message() string { 18 | return "service name should have suffix" 19 | } 20 | 21 | // Validate enforces that all services are suffixed with Service. 22 | func (s *ServiceSuffix) Validate(protoInfo core.ProtoInfo) ([]core.Issue, error) { 23 | var res []core.Issue 24 | 25 | for _, service := range protoInfo.Info.ProtoBody.Services { 26 | if !strings.HasSuffix(service.ServiceName, s.Suffix) { 27 | res = core.AppendIssue(res, s, service.Meta.Pos, service.ServiceName, service.Comments) 28 | } 29 | } 30 | 31 | return res, nil 32 | } 33 | -------------------------------------------------------------------------------- /internal/rules/service_suffix_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yoheimuta/go-protoparser/v4/parser/meta" 8 | 9 | "github.com/easyp-tech/easyp/internal/core" 10 | "github.com/easyp-tech/easyp/internal/rules" 11 | ) 12 | 13 | func TestServiceSuffix_Message(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := require.New(t) 17 | 18 | const expMessage = "service name should have suffix" 19 | 20 | rule := rules.ServiceSuffix{} 21 | message := rule.Message() 22 | 23 | assert.Equal(expMessage, message) 24 | } 25 | 26 | func TestServiceSuffix_Validate(t *testing.T) { 27 | t.Parallel() 28 | 29 | tests := map[string]struct { 30 | fileName string 31 | wantIssues *core.Issue 32 | wantErr error 33 | }{ 34 | "invalid": { 35 | fileName: invalidAuthProto, 36 | wantIssues: &core.Issue{ 37 | Position: meta.Position{ 38 | Filename: "", 39 | Offset: 197, 40 | Line: 10, 41 | Column: 1, 42 | }, 43 | SourceName: "auth", 44 | Message: "service name should have suffix", 45 | RuleName: "SERVICE_SUFFIX", 46 | }, 47 | }, 48 | "valid": { 49 | fileName: validAuthProto, 50 | wantErr: nil, 51 | }, 52 | } 53 | 54 | for name, tc := range tests { 55 | name, tc := name, tc 56 | t.Run(name, func(t *testing.T) { 57 | t.Parallel() 58 | 59 | r, protos := start(t) 60 | 61 | rule := rules.ServiceSuffix{ 62 | Suffix: "API", 63 | } 64 | issues, err := rule.Validate(protos[tc.fileName]) 65 | r.ErrorIs(err, tc.wantErr) 66 | switch { 67 | case tc.wantIssues != nil: 68 | r.Contains(issues, *tc.wantIssues) 69 | case len(issues) > 0: 70 | r.Empty(issues) 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import "runtime/debug" 4 | 5 | // System returns application version based on build info. 6 | func System() string { 7 | bi, ver := buildVersion() 8 | switch { 9 | case bi == nil: 10 | return "(unknown)" 11 | case ver == "" || bi.Main.Version != "(devel)": 12 | return bi.Main.Version 13 | default: 14 | return ver 15 | } 16 | } 17 | 18 | func buildVersion() (bi *debug.BuildInfo, ver string) { 19 | bi, ok := debug.ReadBuildInfo() 20 | if !ok { 21 | return nil, "" 22 | } 23 | const revisionPrefix = 7 24 | revision := buildSetting(bi, "vcs.revision") 25 | modified := buildSetting(bi, "vcs.modified") 26 | time := buildSetting(bi, "vcs.time") 27 | if revision == "" { 28 | return bi, time 29 | } else if len(revision) > revisionPrefix { 30 | revision = revision[:revisionPrefix] 31 | } 32 | if modified != "false" { 33 | revision += "-modified" 34 | } 35 | if time == "" { 36 | return bi, revision 37 | } 38 | 39 | return bi, revision + " " + time 40 | } 41 | 42 | func buildSetting(bi *debug.BuildInfo, key string) string { 43 | for _, s := range bi.Settings { 44 | if s.Key == key { 45 | return s.Value 46 | } 47 | } 48 | 49 | return "" 50 | } 51 | -------------------------------------------------------------------------------- /testdata/api/session/v1/events.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package api.session.v1; 4 | 5 | import "google/rpc/code.proto"; 6 | 7 | option csharp_namespace = "ZergsLaw.BackTemplate.Api.Session.V1"; 8 | option go_package = "github.com/ZergsLaw/back-template/api/session/v1;pb"; 9 | option java_multiple_files = true; 10 | option java_package = "com.zergslaw.backtemplate.api.session.v1"; 11 | option php_namespace = "ZergsLaw\\BackTemplate\\Api\\Session\\V1"; 12 | option ruby_package = "ZergsLaw"; 13 | option swift_prefix = "ZergsLaw.BackTemplate.Api.Session.V1"; 14 | 15 | // Events, which can be sent by service. 16 | message Event {} 17 | -------------------------------------------------------------------------------- /testdata/auth/InvalidName.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package Empty; 4 | 5 | import "google/protobuf/empty.proto"; 6 | import "google/rpc/code.proto"; 7 | 8 | option go_package = "github.com/ZergsLaw/back-template/api/session/v1;pb"; 9 | 10 | message Queue {} -------------------------------------------------------------------------------- /testdata/auth/empty_pkg.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; -------------------------------------------------------------------------------- /testdata/auth/queue.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package Queue; 4 | 5 | import "google/protobuf/empty.proto"; 6 | import "google/rpc/code.proto"; 7 | 8 | option go_package = "github.com/ZergsLaw/back-template/api/session/v1;pb"; 9 | 10 | message Queue {} -------------------------------------------------------------------------------- /testdata/auth/service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package Session; 4 | 5 | import weak "google/protobuf/empty.proto"; 6 | import public "google/rpc/code.proto"; 7 | 8 | option go_package = "github.com/ZergsLaw/back-template/api/session/v1;pb"; 9 | 10 | service auth { 11 | rpc Save(SessionInfo) returns (Result) {} 12 | rpc Get(TokenData) returns (Session) {} 13 | rpc delete(stream Delete_Info) returns (stream google.protobuf.Empty) {} 14 | rpc GetByToken(TokenData) returns (Session) {} 15 | } 16 | 17 | message TokenData { 18 | string token = 1; 19 | } 20 | 21 | message Session { 22 | string session_id = 1; 23 | string user_id = 2; 24 | } 25 | 26 | message Delete_Info { 27 | string Session_id = 1; 28 | } 29 | 30 | message SessionInfo { 31 | enum social_network { 32 | option allow_alias = true; 33 | none = 4; 34 | } 35 | 36 | string user_id = 1; 37 | string ip = 2; 38 | string user_agent = 3; 39 | oneof SocialNetwork { 40 | string email = 4; 41 | string username = 5; 42 | } 43 | } 44 | 45 | message Result { 46 | string token = 1; 47 | } 48 | 49 | enum social_network { 50 | option allow_alias = true; 51 | none = 4; 52 | } 53 | 54 | 55 | // Product type. 56 | enum ProductType { 57 | // For unknown value. 58 | PRODUCT_TYPE_NONE = 0; 59 | // Calculator. 60 | PRODUCT_TYPE_CALCULATOR = 1; 61 | // Mailing. 62 | PRODUCT_TYPE_MAILING = 2; 63 | } -------------------------------------------------------------------------------- /testdata/breaking_check/broken/messages.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package breaking; 4 | 5 | message RPC1Request { 6 | string field_1 = 11; 7 | } 8 | 9 | message RPC1Response { 10 | string field_1 = 1; 11 | } 12 | -------------------------------------------------------------------------------- /testdata/breaking_check/broken/services.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package breaking; 4 | 5 | enum SomeEnum { 6 | CORPUS_UNSPECIFIED = 0; 7 | CORPUS_UNIVERSAL = 1; 8 | } 9 | 10 | message RPC2Request { 11 | string field_1 = 1; 12 | } 13 | 14 | message MessageForType { 15 | string message_for_type = 1; 16 | } 17 | 18 | message RPC2Response { 19 | message RPC2ResponseNested { 20 | }; 21 | 22 | optional MessageForType field_1 = 1; 23 | 24 | oneof login { 25 | string field = 3; 26 | string fff = 2; 27 | } 28 | } 29 | 30 | message AuthInfo { 31 | string username = 1; 32 | } 33 | 34 | message AuthResponse { 35 | 36 | } 37 | 38 | // Сервис-фасад для DAST сканнера 39 | service Service { 40 | rpc Auth(AuthInfo) returns(AuthResponse); 41 | // RPC1 42 | rpc RPC1(RPC1Request) returns(RPC1Response); 43 | } 44 | -------------------------------------------------------------------------------- /testdata/breaking_check/not_broken/messages.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package breaking; 4 | 5 | message RPC1Request { 6 | string field_1 = 1; 7 | string field_2 = 2; 8 | } 9 | 10 | message RPC1Response { 11 | string field_1 = 1; 12 | string field_2 = 2; 13 | } 14 | -------------------------------------------------------------------------------- /testdata/breaking_check/not_broken/services.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package breaking; 4 | 5 | import "messages.proto"; 6 | 7 | enum SomeEnum { 8 | CORPUS_UNSPECIFIED = 0; 9 | CORPUS_UNIVERSAL = 1; 10 | CORPUS_WEB = 2; 11 | CORPUS_WS = 3; 12 | } 13 | 14 | message RPC2Request { 15 | string field_1 = 1; 16 | } 17 | 18 | message MessageForType { 19 | string message_for_type = 1; 20 | } 21 | 22 | message RPC2Response { 23 | message RPC2ResponseNested { 24 | string rpc2_response_nested_field = 1; 25 | }; 26 | 27 | optional MessageForType field_1 = 1; 28 | 29 | oneof login { 30 | string field = 3; 31 | string fff = 2; 32 | RPC2ResponseNested rrr = 4; 33 | } 34 | } 35 | 36 | message AuthInfo { 37 | string username = 1; 38 | string password = 2; 39 | } 40 | 41 | message AuthResponse { 42 | 43 | } 44 | 45 | // Сервис-фасад для DAST сканнера 46 | service Service { 47 | rpc Auth(AuthInfo) returns(AuthResponse); 48 | // RPC1 49 | rpc RPC1(RPC1Request) returns(RPC1Response); 50 | // RPC2 51 | rpc RPC2(RPC2Request) returns(RPC2Response); 52 | } 53 | -------------------------------------------------------------------------------- /testdata/breaking_check/original/messages.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package breaking; 4 | 5 | message RPC1Request { 6 | string field_1 = 1; 7 | } 8 | 9 | message RPC1Response { 10 | string field_1 = 1; 11 | } 12 | -------------------------------------------------------------------------------- /testdata/breaking_check/original/services.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package breaking; 4 | 5 | import "messages.proto"; 6 | 7 | enum SomeEnum { 8 | CORPUS_UNSPECIFIED = 0; 9 | CORPUS_UNIVERSAL = 1; 10 | CORPUS_WEB = 2; 11 | } 12 | 13 | message RPC2Request { 14 | string field_1 = 1; 15 | } 16 | 17 | message MessageForType { 18 | string message_for_type = 1; 19 | } 20 | 21 | message RPC2Response { 22 | message RPC2ResponseNested { 23 | string rpc2_response_nested_field = 1; 24 | }; 25 | 26 | optional MessageForType field_1 = 1; 27 | 28 | oneof login { 29 | string field = 3; 30 | string fff = 2; 31 | RPC2ResponseNested rrr = 4; 32 | } 33 | } 34 | 35 | message AuthInfo { 36 | string username = 1; 37 | string password = 2; 38 | } 39 | 40 | message AuthResponse { 41 | 42 | } 43 | 44 | // Сервис-фасад для DAST сканнера 45 | service Service { 46 | rpc Auth(AuthInfo) returns(AuthResponse); 47 | // RPC1 48 | rpc RPC1(RPC1Request) returns(RPC1Response); 49 | // RPC2 50 | rpc RPC2(RPC2Request) returns(RPC2Response); 51 | } 52 | -------------------------------------------------------------------------------- /testdata/import_used/enums.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_used; 4 | 5 | enum SomeEnum { 6 | SOME_ENUM_VALUE = 0; 7 | } -------------------------------------------------------------------------------- /testdata/import_used/field.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_used; 4 | 5 | message SomeField { 6 | string field = 1; 7 | } -------------------------------------------------------------------------------- /testdata/import_used/for_one_of.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_used; 4 | 5 | message ForOneOf { 6 | string field = 1; 7 | } -------------------------------------------------------------------------------- /testdata/import_used/messages.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_used; 4 | 5 | message MessageRequest {} 6 | 7 | message MessageResponse {} 8 | -------------------------------------------------------------------------------- /testdata/import_used/not_used.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "import_used/messages.proto"; 4 | 5 | package import_used; 6 | 7 | message Msg {} 8 | 9 | service TestService { 10 | rpc TestRPC(Msg) returns(Msg) {} 11 | } -------------------------------------------------------------------------------- /testdata/import_used/options.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_used; 4 | 5 | message Option { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /testdata/import_used/thrd_party/enums.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_used.thrd_party; 4 | 5 | enum SomeEnum { 6 | SOME_ENUM_VALUE = 0; 7 | } -------------------------------------------------------------------------------- /testdata/import_used/thrd_party/for_extends.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_used.thrd_party; 4 | 5 | message MethodOptions { 6 | optional bool deprecated = 33 [default = false]; 7 | } 8 | -------------------------------------------------------------------------------- /testdata/import_used/thrd_party/for_option.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_used.thrd_party; 4 | 5 | message ForExtend { 6 | } 7 | -------------------------------------------------------------------------------- /testdata/import_used/thrd_party/messages.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_used.thrd_party; 4 | 5 | message MessageRequest { 6 | 7 | } 8 | 9 | message MessageResponse { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /testdata/import_used/thrd_party/options.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_used.thrd_party; 4 | 5 | message Option { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /testdata/import_used/thrd_party/types.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_used.thrd_party; 4 | 5 | message MessageAsType {} 6 | -------------------------------------------------------------------------------- /testdata/import_used/types.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_used; 4 | 5 | message MessageAsType {} 6 | -------------------------------------------------------------------------------- /testdata/import_used/used.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "import_used/messages.proto"; 4 | import "import_used/enums.proto"; 5 | import "import_used/options.proto"; 6 | import "import_used/types.proto"; 7 | import "import_used/field.proto"; 8 | import "import_used/for_one_of.proto"; 9 | import "import_used/thrd_party/messages.proto"; 10 | import "import_used/thrd_party/enums.proto"; 11 | import "import_used/thrd_party/options.proto"; 12 | import "import_used/thrd_party/types.proto"; 13 | import "import_used/thrd_party/for_option.proto"; 14 | 15 | package import_used; 16 | 17 | extend import_used.thrd_party.MethodOptions { 18 | MethodOption options = 10000; 19 | } 20 | 21 | option (import_used.thrd_party.ForExtend) = {}; 22 | 23 | message SomeMessage { 24 | // use import from the same package 25 | SomeEnum field_1 = 1; 26 | MessageAsType field_2 = 2; 27 | 28 | // from 3rd party 29 | import_used.thrd_party.SomeEnum field_3 = 3; 30 | import_used.thrd_party.MessageAsType field_4 = 4; 31 | 32 | message Nested { 33 | SomeField in_nested = 1; 34 | 35 | oneof oneof{ 36 | ForOneOf one_of = 3; 37 | } 38 | } 39 | } 40 | 41 | service TestService { 42 | rpc TestRPCSamePackage(MessageRequest) returns(MessageResponse) { 43 | option (Option) = {}; 44 | } 45 | 46 | rpc TestRPCThrdPartyPackage(import_used.thrd_party.MessageRequest) returns(import_used.thrd_party.MessageResponse) { 47 | option (import_used.thrd_party.Option) = {}; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /testdata/invalid_options/queue.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package same_package; 4 | 5 | import "google/protobuf/empty.proto"; 6 | import "google/rpc/code.proto"; 7 | 8 | option csharp_namespace = "ZergsLaw.BackTemplate.Api.Session.V1"; 9 | option go_package = "github.com/ZergsLaw/back-template/api/session/v1;pb"; 10 | option java_multiple_files = true; 11 | option java_package = "com.zergslaw.backtemplate.api.session.v1"; 12 | option php_namespace = "ZergsLaw\\BackTemplate\\Api\\Session\\V1"; 13 | option ruby_package = "ZergsLaw"; 14 | option swift_prefix = "ZergsLaw.BackTemplate.Api.Session.V1"; 15 | 16 | message Queue {} 17 | -------------------------------------------------------------------------------- /testdata/invalid_pkg/queue.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package Queue; 4 | 5 | import "google/protobuf/empty.proto"; 6 | import "google/rpc/code.proto"; 7 | 8 | option go_package = "github.com/ZergsLaw/back-template/api/session/v1;pb"; 9 | 10 | message Queue {} -------------------------------------------------------------------------------- /testdata/invalid_pkg/session.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package api.session.v1; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | import "google/protobuf/some.proto"; 7 | 8 | option go_package = "github.com/ZergsLaw/back-template/api/session/v1;pb"; 9 | 10 | // Internal service API for managing user session. 11 | service SessionInternalAPI { 12 | // Save new user's session and returns auth token. 13 | rpc Save(SaveRequest) returns (SaveResponse) {} 14 | 15 | // Returns user's session info by token. 16 | rpc Get(GetRequest) returns (GetResponse) {} 17 | 18 | // Delete user's session by auth token. 19 | rpc Delete(DeleteRequest) returns (DeleteResponse) {} 20 | } 21 | 22 | //---Must be filled out--- 23 | message GetRequest { 24 | // Contains auth token, which was send by Save handler. 25 | string token = 1; 26 | } 27 | 28 | //---Must be filled out--- 29 | message GetResponse { 30 | // Contains session's UUID. 31 | string session_id = 1; 32 | // Contains user's UUID. 33 | string user_id = 2; 34 | // Contains user's session start time. 35 | google.protobuf.Timestamp created_at = 3; 36 | } 37 | 38 | //---Must be filled out--- 39 | message DeleteRequest { 40 | // Contains session's UUID. 41 | string session_id = 1; 42 | } 43 | 44 | //---Must be filled out--- 45 | message DeleteResponse {} 46 | 47 | //---Must be filled out--- 48 | message SaveRequest { 49 | // Contains user UUID. 50 | string user_id = 1; 51 | // Contains user's origin IP. 52 | string ip = 2; 53 | // Contains user's client. 54 | string user_agent = 3; 55 | } 56 | 57 | //---Must be filled out--- 58 | message SaveResponse { 59 | // User's auth token. 60 | string token = 1; 61 | } 62 | 63 | //---Must be filled out--- 64 | enum SocialNetwork { 65 | //---Must be filled out--- 66 | SOCIAL_NETWORK_NONE = 0; 67 | //---Must be filled out--- 68 | SOCIAL_NETWORK_GOOGLE = 1; 69 | //---Must be filled out--- 70 | SOCIAL_NETWORK_YAHOO = 2; 71 | //---Must be filled out--- 72 | SOCIAL_NETWORK_FACEBOOK = 3; 73 | } 74 | -------------------------------------------------------------------------------- /testdata/no_lint/no_lint_buf_comment.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package no_lint; 4 | 5 | // some commenct 6 | // buf:lint:ignore MESSAGE_PASCAL_CASE 7 | message some_message {} 8 | -------------------------------------------------------------------------------- /testdata/no_lint/no_lint_easyp_comment.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package no_lint; 4 | 5 | // some commenct 6 | // nolint:MESSAGE_PASCAL_CASE 7 | message some_message {} 8 | -------------------------------------------------------------------------------- /wellknownimports/embed.go: -------------------------------------------------------------------------------- 1 | package wellknownimports 2 | 3 | import ( 4 | "embed" 5 | ) 6 | 7 | //go:embed * 8 | var Content embed.FS 9 | -------------------------------------------------------------------------------- /wellknownimports/google/protobuf/empty.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.protobuf; 18 | 19 | option go_package = "google.golang.org/protobuf/types/known/emptypb"; 20 | option java_package = "com.google.protobuf"; 21 | option java_outer_classname = "EmptyProto"; 22 | option java_multiple_files = true; 23 | option objc_class_prefix = "GPB"; 24 | option csharp_namespace = "Google.Protobuf.WellKnownTypes"; 25 | option cc_enable_arenas = true; 26 | 27 | // A generic empty message that you can re-use to avoid defining duplicated 28 | // empty messages in your APIs. A typical example is to use it as the request 29 | // or the response type of an API method. For instance: 30 | // 31 | // service Foo { 32 | // rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); 33 | // } 34 | // 35 | message Empty {} 36 | -------------------------------------------------------------------------------- /wellknownimports/google/protobuf/source_context.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.protobuf; 18 | 19 | option java_package = "com.google.protobuf"; 20 | option java_outer_classname = "SourceContextProto"; 21 | option java_multiple_files = true; 22 | option objc_class_prefix = "GPB"; 23 | option csharp_namespace = "Google.Protobuf.WellKnownTypes"; 24 | option go_package = "google.golang.org/protobuf/types/known/sourcecontextpb"; 25 | 26 | // `SourceContext` represents information about the source of a 27 | // protobuf element, like the file in which it is defined. 28 | message SourceContext { 29 | // The path-qualified name of the .proto file that contained the associated 30 | // protobuf element. For example: `"google/protobuf/source_context.proto"`. 31 | string file_name = 1; 32 | } 33 | --------------------------------------------------------------------------------