assessment/collectors/parser/response_parser.go (170 lines of code) (raw):

/* Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. */ package assessment import ( "encoding/json" "fmt" "strconv" "strings" . "github.com/GoogleCloudPlatform/spanner-migration-tool/assessment/utils" "github.com/GoogleCloudPlatform/spanner-migration-tool/logger" "go.uber.org/zap" ) // ParseStringArrayInterface Parse input into []string. Validate the type of input: // If input is of type string, then a string array with 1 element is returned. // If input is of string array, then the parsed string array is returned. func ParseStringArrayInterface(input any) []string { switch input := input.(type) { case []string: return input case string: return []string{input} case []any: parsedStringArray := make([]string, 0, len(input)) for _, parsedInputLine := range input { if parsedInputLine == nil { logger.Log.Error("Error in parsing string array:", zap.Any("any", input)) continue } switch parsedInputLine := parsedInputLine.(type) { case string: parsedStringArray = append(parsedStringArray, parsedInputLine) default: logger.Log.Error("Error in parsing string array:", zap.Any("any", input)) continue } } return parsedStringArray default: logger.Log.Error("Error in parsing string array:", zap.Any("any", input)) return []string{} } } func parseAnyToString(anyType any) string { return fmt.Sprintf("%v", anyType) } func parseAnyToInteger(anyType any) int { str := parseAnyToString(anyType) i, err := strconv.Atoi(str) if err != nil { logger.Log.Debug("could not parse string to int" + str) return 0 } return i } func ParseSchemaImpact(schemaImpactResponse map[string]any, projectPath, filePath string) (*Snippet, error) { logger.Log.Debug("schemaImpactResponse:", zap.Any("sec: ", schemaImpactResponse)) return &Snippet{ SchemaChange: parseAnyToString(schemaImpactResponse["schema_change"]), TableName: parseAnyToString(schemaImpactResponse["table"]), ColumnName: parseAnyToString(schemaImpactResponse["column"]), NumberOfAffectedLines: parseAnyToInteger(schemaImpactResponse["number_of_affected_lines"]), SourceCodeSnippet: ParseStringArrayInterface(schemaImpactResponse["existing_code_lines"]), SuggestedCodeSnippet: ParseStringArrayInterface(schemaImpactResponse["new_code_lines"]), RelativeFilePath: getRelativeFilePath(projectPath, filePath), FilePath: filePath, IsDao: true, }, nil } func ParseCodeImpact(codeImpactResponse map[string]any, projectPath, filePath string) (*Snippet, error) { //To check if it is mandatory for the response to contain these methods return &Snippet{ SourceMethodSignature: parseAnyToString(codeImpactResponse["original_method_signature"]), SuggestedMethodSignature: parseAnyToString(codeImpactResponse["new_method_signature"]), SourceCodeSnippet: ParseStringArrayInterface(codeImpactResponse["code_sample"]), SuggestedCodeSnippet: ParseStringArrayInterface(codeImpactResponse["suggested_change"]), NumberOfAffectedLines: parseAnyToInteger(codeImpactResponse["number_of_affected_lines"]), Complexity: parseAnyToString(codeImpactResponse["complexity"]), Explanation: parseAnyToString(codeImpactResponse["description"]), RelativeFilePath: getRelativeFilePath(projectPath, filePath), FilePath: filePath, IsDao: false, }, nil } func getRelativeFilePath(projectPath, filePath string) string { relativeFilePath := filePath if strings.HasPrefix(filePath, projectPath) { relativeFilePath = strings.Replace(filePath, projectPath, "", 1) } return relativeFilePath } func ParseNonDaoFileChanges(fileAnalyzerResponse string, projectPath, filePath string, fileIndex int) ([]Snippet, []string, error) { var result map[string]any err := json.Unmarshal([]byte(fileAnalyzerResponse), &result) if err != nil { return nil, nil, err } snippets := []Snippet{} codeSnippetIndex := 0 for _, codeImpactResponse := range result["file_modifications"].([]any) { codeImpact, err := ParseCodeImpact(codeImpactResponse.(map[string]any), projectPath, filePath) if err != nil { return nil, nil, err } codeImpact.Id = fmt.Sprintf("snippet_%d_%d", fileIndex, codeSnippetIndex) snippets = append(snippets, *codeImpact) codeSnippetIndex++ } generalWarnings := []string{} if result["general_warnings"] != nil { generalWarnings = ParseStringArrayInterface(result["general_warnings"].([]any)) } return snippets, generalWarnings, nil } func ParseDaoFileChanges(fileAnalyzerResponse string, projectPath, filePath string, fileIndex int) ([]Snippet, []string, error) { var result map[string]any err := json.Unmarshal([]byte(fileAnalyzerResponse), &result) if err != nil { return nil, nil, err } snippets := []Snippet{} codeSnippetIndex := 0 for _, schemaImpactResponse := range result["schema_impact"].([]any) { codeSchemaImpact, err := ParseSchemaImpact(schemaImpactResponse.(map[string]any), projectPath, filePath) if err != nil { return nil, nil, err } if isCodeEqual(&codeSchemaImpact.SourceCodeSnippet, &codeSchemaImpact.SuggestedCodeSnippet) { logger.Log.Debug("not emmitting as code snippets are equal") } else { codeSchemaImpact.Id = fmt.Sprintf("snippet_%d_%d", fileIndex, codeSnippetIndex) snippets = append(snippets, *codeSchemaImpact) codeSnippetIndex++ } } generalWarnings := []string{} if result["general_warnings"] != nil { generalWarnings = ParseStringArrayInterface(result["general_warnings"].([]any)) } return snippets, generalWarnings, nil } func isCodeEqual(sourceCode *[]string, suggestedCode *[]string) bool { if *sourceCode == nil && *suggestedCode == nil { return true } else if *sourceCode == nil || suggestedCode == nil { return false } srcCode := "" for _, codeLine := range *sourceCode { srcCode += strings.TrimSpace(codeLine) } sugCode := "" for _, codeLine := range *suggestedCode { sugCode += strings.TrimSpace(codeLine) } return srcCode == sugCode } func ParseFileAnalyzerResponse(projectPath, filePath, fileAnalyzerResponse string, isDao bool, fileIndex int) (*CodeAssessment, error) { var snippets []Snippet var err error var generalWarnings []string if isDao { //This logic is incorrect - the dependent files need to show up as schema impact snippets, generalWarnings, err = ParseDaoFileChanges(fileAnalyzerResponse, projectPath, filePath, fileIndex) } else { snippets, generalWarnings, err = ParseNonDaoFileChanges(fileAnalyzerResponse, projectPath, filePath, fileIndex) if err != nil { return nil, err } } if err != nil { return nil, err } return &CodeAssessment{ Snippets: &snippets, GeneralWarnings: generalWarnings, }, nil }