diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..5d78999 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,22 @@ +name: golangci-lint + +on: + push: + branches: + - main + pull_request: + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.22' + cache: false + - name: golangci-lint + uses: golangci/golangci-lint-action@v4 + with: + version: v1.57.2 diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..7c6c123 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,52 @@ +run: + timeout: 5m + issues-exit-code: 1 + tests: true + modules-download-mode: readonly + +linters: + disable-all: true + enable: + - bodyclose + - contextcheck + - gofmt + - errcheck + - errorlint + - gocritic + - godot + - gosec + - gosimple + - govet + - ineffassign + - misspell + - revive + - staticcheck + - typecheck + - unused + - nilerr + - tparallel + - unparam + - whitespace + - bidichk + - exportloopref + - goconst + - reassign + - goimports + - exhaustive + - gci + - gomodguard + - prealloc + - forbidigo + - dogsled + - nakedret + - stylecheck + - unconvert + - gocyclo + - gosec + - nolintlint + - forcetypeassert + - gochecknoglobals + - asciicheck + - goprintffuncname + - nestif + - noctx diff --git a/crowdin/branches_test.go b/crowdin/branches_test.go index 0b73d05..8180991 100644 --- a/crowdin/branches_test.go +++ b/crowdin/branches_test.go @@ -2,6 +2,7 @@ package crowdin import ( "context" + "errors" "fmt" "net/http" "reflect" @@ -181,7 +182,9 @@ func TestBranchesService_GetByIDNotFound(t *testing.T) { if resp.StatusCode != http.StatusNotFound { t.Errorf("Branches.Get expected status 404, got %v", resp.StatusCode) } - if e, ok := err.(*model.ErrorResponse); !ok { + + var e *model.ErrorResponse + if !errors.As(err, &e) { t.Errorf("Branches.Get expected *model.ErrorResponse, got %+v", e) } } diff --git a/crowdin/crowdin.go b/crowdin/crowdin.go index 7e706af..77731ce 100644 --- a/crowdin/crowdin.go +++ b/crowdin/crowdin.go @@ -134,7 +134,7 @@ func (c *Client) newRequest(ctx context.Context, method, path string, body any, return nil, err } } - req, err := http.NewRequest(method, u.String(), buf) + req, err := http.NewRequestWithContext(ctx, method, u.String(), buf) if err != nil { return nil, err } @@ -151,8 +151,6 @@ func (c *Client) newRequest(ctx context.Context, method, path string, body any, } } - req = req.WithContext(ctx) - return req, nil } diff --git a/crowdin/crowdin_test.go b/crowdin/crowdin_test.go index b67ecf6..a1ee6ff 100644 --- a/crowdin/crowdin_test.go +++ b/crowdin/crowdin_test.go @@ -147,7 +147,6 @@ func TestNewEnterpriseClient(t *testing.T) { func TestWithCustomHTTPClient(t *testing.T) { c, err := NewClient("token", WithHTTPClient(http.DefaultClient)) - if err != nil { t.Errorf("NewClient error: %v", err) } diff --git a/crowdin/groups_test.go b/crowdin/groups_test.go index cbd64ad..7a459fb 100644 --- a/crowdin/groups_test.go +++ b/crowdin/groups_test.go @@ -334,6 +334,7 @@ func TestGroupService_Delete(t *testing.T) { mux.HandleFunc("/api/v2/groups/2", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "DELETE") testURL(t, r, "/api/v2/groups/2") + w.WriteHeader(http.StatusNoContent) }) _, err := client.Groups.Delete(context.Background(), 2) diff --git a/crowdin/languages_test.go b/crowdin/languages_test.go index a3a22de..eed8adc 100644 --- a/crowdin/languages_test.go +++ b/crowdin/languages_test.go @@ -236,7 +236,7 @@ func TestLanguagesService_GetByLanguageIDNotFound(t *testing.T) { client, mux, teardown := setupClient() defer teardown() - mux.HandleFunc("/api/v2/languages/xx", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/v2/languages/xx", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) }) @@ -381,6 +381,7 @@ func TestLanguagesService_Delete(t *testing.T) { mux.HandleFunc("/api/v2/languages/uk", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "DELETE") testURL(t, r, "/api/v2/languages/uk") + w.WriteHeader(http.StatusNoContent) }) _, err := client.Languages.Delete(context.Background(), "uk") diff --git a/crowdin/model/branches.go b/crowdin/model/branches.go index 3510928..b2c8fbe 100644 --- a/crowdin/model/branches.go +++ b/crowdin/model/branches.go @@ -1,7 +1,7 @@ package model import ( - "fmt" + "errors" "net/url" ) @@ -80,7 +80,7 @@ func (r *BranchesAddRequest) Validate() error { return ErrNilRequest } if r.Name == "" { - return fmt.Errorf("name is required") + return errors.New("name is required") } return nil } @@ -136,7 +136,7 @@ func (r *BranchesMergeRequest) Validate() error { return ErrNilRequest } if r.SourceBranchID == 0 { - return fmt.Errorf("sourceBranchId is required") + return errors.New("sourceBranchId is required") } return nil } @@ -157,7 +157,7 @@ func (r *BranchesCloneRequest) Validate() error { return ErrNilRequest } if r.Name == "" { - return fmt.Errorf("name is required") + return errors.New("name is required") } return nil } diff --git a/crowdin/model/source_files.go b/crowdin/model/source_files.go index 7332b0c..1aec465 100644 --- a/crowdin/model/source_files.go +++ b/crowdin/model/source_files.go @@ -1,6 +1,7 @@ package model import ( + "errors" "fmt" "net/url" ) @@ -68,8 +69,8 @@ func (o *DirectoryListOptions) Values() (url.Values, bool) { if o.Filter != "" { v.Add("filter", o.Filter) } - if o.Recursion != nil { - v.Add("recursion", o.Recursion.(string)) + if recursion, ok := o.Recursion.(string); ok { + v.Add("recursion", recursion) } return v, len(v) > 0 @@ -106,10 +107,10 @@ func (r *DirectoryAddRequest) Validate() error { return ErrNilRequest } if r.Name == "" { - return fmt.Errorf("name is required") + return errors.New("name is required") } if r.BranchID != 0 && r.DirectoryID != 0 { - return fmt.Errorf("branchId and directoryId cannot be used in the same request") + return errors.New("branchId and directoryId cannot be used in the same request") } return nil } @@ -185,8 +186,8 @@ func (o *FileListOptions) Values() (url.Values, bool) { if o.Filter != "" { v.Add("filter", o.Filter) } - if o.Recursion != nil { - v.Add("recursion", o.Recursion.(string)) + if recursion, ok := o.Recursion.(string); ok { + v.Add("recursion", recursion) } return v, len(v) > 0 @@ -240,13 +241,13 @@ func (r *FileAddRequest) Validate() error { return ErrNilRequest } if r.StorageID == 0 { - return fmt.Errorf("storageId is required") + return errors.New("storageId is required") } if r.Name == "" { - return fmt.Errorf("name is required") + return errors.New("name is required") } if r.BranchID > 0 && r.DirectoryID > 0 { - return fmt.Errorf("branchId and directoryId cannot be used in the same request") + return errors.New("branchId and directoryId cannot be used in the same request") } return nil } @@ -469,10 +470,10 @@ func (r *FileUpdateRestoreRequest) Validate() error { return ErrNilRequest } if r.RevisionID == 0 && r.StorageID == 0 { - return fmt.Errorf("one of revisionId or storageId is required") + return errors.New("one of revisionId or storageId is required") } if r.RevisionID != 0 && r.StorageID != 0 { - return fmt.Errorf("use only one of revisionId or storageId") + return errors.New("use only one of revisionId or storageId") } return nil } diff --git a/crowdin/model/string_translations.go b/crowdin/model/string_translations.go index a50163d..aa3771c 100644 --- a/crowdin/model/string_translations.go +++ b/crowdin/model/string_translations.go @@ -1,6 +1,7 @@ package model import ( + "errors" "fmt" "net/url" "strings" @@ -131,16 +132,16 @@ type TranslationAlignmentRequest struct { // It implements the crowdin.RequestValidator interface. func (r *TranslationAlignmentRequest) Validate() error { if r == nil { - return fmt.Errorf("request cannot be nil") + return errors.New("request cannot be nil") } if r.SourceLanguageID == "" { - return fmt.Errorf("source language ID is required") + return errors.New("source language ID is required") } if r.TargetLanguageID == "" { - return fmt.Errorf("target language ID is required") + return errors.New("target language ID is required") } if r.Text == "" { - return fmt.Errorf("text is required") + return errors.New("text is required") } return nil } @@ -345,16 +346,16 @@ type TranslationAddRequest struct { // It implements the crowdin.RequestValidator interface. func (r *TranslationAddRequest) Validate() error { if r == nil { - return fmt.Errorf("request cannot be nil") + return errors.New("request cannot be nil") } if r.StringID == 0 { - return fmt.Errorf("string ID is required") + return errors.New("string ID is required") } if r.LanguageID == "" { - return fmt.Errorf("language ID is required") + return errors.New("language ID is required") } if r.Text == "" { - return fmt.Errorf("text is required") + return errors.New("text is required") } return nil } @@ -457,21 +458,21 @@ type VoteAddRequest struct { // It implements the crowdin.RequestValidator interface. func (r *VoteAddRequest) Validate() error { if r == nil { - return fmt.Errorf("request cannot be nil") + return errors.New("request cannot be nil") } if r.Mark != VoteTypeUp && r.Mark != VoteTypeDown { return fmt.Errorf("invalid vote type: %s", r.Mark) } if r.TranslationID == 0 { - return fmt.Errorf("translation ID is required") + return errors.New("translation ID is required") } return nil } func joinIntSlice(s []int) string { - var res []string - for _, v := range s { - res = append(res, fmt.Sprintf("%d", v)) + res := make([]string, len(s)) + for i, v := range s { + res[i] = fmt.Sprintf("%d", v) } return strings.Join(res, ",") } diff --git a/crowdin/model/translations.go b/crowdin/model/translations.go index 2f8b549..4472e82 100644 --- a/crowdin/model/translations.go +++ b/crowdin/model/translations.go @@ -1,6 +1,7 @@ package model import ( + "errors" "fmt" "net/url" ) @@ -86,10 +87,10 @@ func (r *PreTranslationRequest) Validate() error { return ErrNilRequest } if len(r.LanguageIDs) == 0 { - return fmt.Errorf("languageIds is required") + return errors.New("languageIds is required") } if len(r.FileIDs) == 0 { - return fmt.Errorf("fileIds is required") + return errors.New("fileIds is required") } return nil } @@ -121,7 +122,7 @@ func (r *BuildProjectDirectoryTranslationRequest) Validate() error { if (r.SkipUntranslatedStrings != nil && r.SkipUntranslatedFiles != nil) && (*r.SkipUntranslatedStrings && *r.SkipUntranslatedFiles) { - return fmt.Errorf("skipUntranslatedStrings and skipUntranslatedFiles must not be true at the same request") + return errors.New("skipUntranslatedStrings and skipUntranslatedFiles must not be true at the same request") } return nil } @@ -159,12 +160,12 @@ func (r *BuildProjectFileTranslationRequest) Validate() error { return ErrNilRequest } if len(r.TargetLanguageID) == 0 { - return fmt.Errorf("targetLanguageId is required") + return errors.New("targetLanguageId is required") } if (r.SkipUntranslatedStrings != nil && r.SkipUntranslatedFiles != nil) && (*r.SkipUntranslatedStrings && *r.SkipUntranslatedFiles) { - return fmt.Errorf("skipUntranslatedStrings and skipUntranslatedFiles must not be true at the same request") + return errors.New("skipUntranslatedStrings and skipUntranslatedFiles must not be true at the same request") } return nil } @@ -278,7 +279,7 @@ func (r *BuildProjectRequest) ValidateBuildRequest() error { if (r.SkipUntranslatedStrings != nil && r.SkipUntranslatedFiles != nil) && (*r.SkipUntranslatedStrings && *r.SkipUntranslatedFiles) { - return fmt.Errorf("`skipUntranslatedStrings` and `skipUntranslatedFiles` must not be true at the same request") + return errors.New("`skipUntranslatedStrings` and `skipUntranslatedFiles` must not be true at the same request") } return nil } @@ -300,7 +301,7 @@ func (r *PseudoBuildProjectRequest) ValidateBuildRequest() error { if r.LengthTransformation != nil && (*r.LengthTransformation < -50 || *r.LengthTransformation > 100) { - return fmt.Errorf("lengthTransformation must be from -50 to 100") + return errors.New("lengthTransformation must be from -50 to 100") } return nil } @@ -340,10 +341,10 @@ func (r *UploadTranslationsRequest) Validate() error { return ErrNilRequest } if r.StorageID == 0 { - return fmt.Errorf("storageId is required") + return errors.New("storageId is required") } if r.FileID > 0 && r.BranchID > 0 { - return fmt.Errorf("fileId and branchId can not be used at the same request") + return errors.New("fileId and branchId can not be used at the same request") } return nil } @@ -399,7 +400,7 @@ func (r *ExportTranslationRequest) Validate() error { return ErrNilRequest } if r.TargetLanguageID == "" { - return fmt.Errorf("targetLanguageId is required") + return errors.New("targetLanguageId is required") } return nil } diff --git a/crowdin/projects.go b/crowdin/projects.go index 215cc43..3fa60a9 100644 --- a/crowdin/projects.go +++ b/crowdin/projects.go @@ -90,7 +90,8 @@ func (s *ProjectsService) Delete(ctx context.Context, id int) (*Response, error) // // https://developer.crowdin.com/api/v2/#operation/api.projects.file-format-settings.custom-segmentations.get func (s *ProjectsService) DownloadFileFormatSettingsCustomSegmentation(ctx context.Context, projectID, settingsID int) ( - *model.DownloadLink, *Response, error) { + *model.DownloadLink, *Response, error, +) { path := fmt.Sprintf("/api/v2/projects/%d/file-format-settings/%d/custom-segmentations", projectID, settingsID) res := new(model.DownloadLinkResponse) resp, err := s.client.Get(ctx, path, nil, res) @@ -145,7 +146,8 @@ func (s *ProjectsService) GetFileFormatSettings(ctx context.Context, projectID, // // https://developer.crowdin.com/api/v2/#operation/api.projects.file-format-settings.post func (s *ProjectsService) AddFileFormatSettings(ctx context.Context, projectID int, req *model.ProjectsAddFileFormatSettingsRequest) ( - *model.ProjectsFileFormatSettings, *Response, error) { + *model.ProjectsFileFormatSettings, *Response, error, +) { path := fmt.Sprintf("/api/v2/projects/%d/file-format-settings", projectID) res := new(model.ProjectsFileFormatSettingsResponse) resp, err := s.client.Post(ctx, path, req, res) diff --git a/crowdin/translation_status_test.go b/crowdin/translation_status_test.go index db19744..8eb28c2 100644 --- a/crowdin/translation_status_test.go +++ b/crowdin/translation_status_test.go @@ -10,54 +10,6 @@ import ( "github.com/crowdin/crowdin-api-client-go/crowdin/model" ) -var jsonRespMock = `{ - "data": [ - { - "data": { - "words": { - "total": 7249, - "translated": 3651, - "preTranslateAppliedTo": 1254, - "approved": 3637 - }, - "phrases": { - "total": 3041, - "translated": 2631, - "preTranslateAppliedTo": 1254, - "approved": 2622 - }, - "translationProgress": 86, - "approvalProgress": 86, - "languageId": "es", - "language": { - "id": "es", - "name": "Spanish", - "editorCode": "es", - "twoLettersCode": "es", - "threeLettersCode": "spa", - "locale": "es-ES", - "androidCode": "es-rES", - "osxCode": "es.lproj", - "osxLocale": "es", - "pluralCategoryNames": [ - "one" - ], - "pluralRules": "(n != 1)", - "pluralExamples": [ - "0, 2-999; 1.2, 2.07..." - ], - "textDirection": "ltr", - "dialectOf": "es" - } - } - } - ], - "pagination": { - "offset": 1, - "limit": 25 - } -}` - func TestTranslationStatusService_GetBranchProgress(t *testing.T) { client, mux, teardown := setupClient() defer teardown() @@ -66,7 +18,7 @@ func TestTranslationStatusService_GetBranchProgress(t *testing.T) { testMethod(t, r, http.MethodGet) testURL(t, r, "/api/v2/projects/1/branches/2/languages/progress?limit=25&offset=1") - fmt.Fprint(w, jsonRespMock) + fmt.Fprint(w, getJSONResponseMock()) }) opts := &model.ListOptions{ @@ -131,7 +83,7 @@ func TestTranslationStatusService_GetDirectoryProgress(t *testing.T) { testMethod(t, r, http.MethodGet) testURL(t, r, "/api/v2/projects/1/directories/2/languages/progress?limit=25&offset=1") - fmt.Fprint(w, jsonRespMock) + fmt.Fprint(w, getJSONResponseMock()) }) opts := &model.ListOptions{ @@ -196,7 +148,7 @@ func TestTranslationStatusService_GetFileProgress(t *testing.T) { testMethod(t, r, http.MethodGet) testURL(t, r, "/api/v2/projects/1/files/2/languages/progress?limit=25&offset=1") - fmt.Fprint(w, jsonRespMock) + fmt.Fprint(w, getJSONResponseMock()) }) opts := &model.ListOptions{ @@ -338,7 +290,7 @@ func TestTranslationStatusService_GetProjectProgress(t *testing.T) { testMethod(t, r, http.MethodGet) testURL(t, r, "/api/v2/projects/1/languages/progress?languageIds=es%2Cde%2Cfr&limit=25&offset=1") - fmt.Fprint(w, jsonRespMock) + fmt.Fprint(w, getJSONResponseMock()) }) opts := &model.ProjectProgressListOptions{ @@ -407,7 +359,7 @@ func TestTranslationStatusService_ListQAChecks(t *testing.T) { mux.HandleFunc(fmt.Sprintf("/api/v2/projects/%d/qa-checks", projectID), func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) q := "category=spellcheck&languageIds=uk%2Cde&limit=25&offset=1&validation=empty_string_check%2Cempty_suggestion_check" - testURL(t, r, "/api/v2/projects/1/qa-checks?" + q) + testURL(t, r, "/api/v2/projects/1/qa-checks?"+q) fmt.Fprint(w, `{ "data": [ @@ -433,8 +385,8 @@ func TestTranslationStatusService_ListQAChecks(t *testing.T) { opts := &model.QACheckListOptions{ ListOptions: model.ListOptions{Limit: 25, Offset: 1}, - Category: []string{"spellcheck"}, - Validation: []string{"empty_string_check,empty_suggestion_check"}, + Category: []string{"spellcheck"}, + Validation: []string{"empty_string_check,empty_suggestion_check"}, LanguageIDs: []string{"uk", "de"}, } issues, resp, err := client.TranslationStatus.ListQAChecks(context.Background(), projectID, opts) @@ -463,3 +415,53 @@ func TestTranslationStatusService_ListQAChecks(t *testing.T) { t.Errorf("TranslationStatus.ListQAChecks pagination returned %+v, want %+v", resp.Pagination, expectedPagination) } } + +func getJSONResponseMock() string { + return `{ + "data": [ + { + "data": { + "words": { + "total": 7249, + "translated": 3651, + "preTranslateAppliedTo": 1254, + "approved": 3637 + }, + "phrases": { + "total": 3041, + "translated": 2631, + "preTranslateAppliedTo": 1254, + "approved": 2622 + }, + "translationProgress": 86, + "approvalProgress": 86, + "languageId": "es", + "language": { + "id": "es", + "name": "Spanish", + "editorCode": "es", + "twoLettersCode": "es", + "threeLettersCode": "spa", + "locale": "es-ES", + "androidCode": "es-rES", + "osxCode": "es.lproj", + "osxLocale": "es", + "pluralCategoryNames": [ + "one" + ], + "pluralRules": "(n != 1)", + "pluralExamples": [ + "0, 2-999; 1.2, 2.07..." + ], + "textDirection": "ltr", + "dialectOf": "es" + } + } + } + ], + "pagination": { + "offset": 1, + "limit": 25 + } + }` +}