github.com/getgauge/gauge@v1.6.9/refactor/refactor.go (about) 1 /*---------------------------------------------------------------- 2 * Copyright (c) ThoughtWorks, Inc. 3 * Licensed under the Apache License, Version 2.0 4 * See LICENSE in the project root for license information. 5 *----------------------------------------------------------------*/ 6 7 /* 8 Given old and new step gives the filenames of specification, concepts and files in code changed. 9 10 Refactoring Flow: 11 - Refactor specs and concepts in memory 12 - Checks if it is a concept or not 13 - In case of concept - writes to file and skips the runner 14 - If its not a concept (its a step) - need to know the text, so makes a call to runner to get the text(step name) 15 - Refactors the text(changes param positions ect) and sends it to runner to refactor implementations. 16 */ 17 package refactor 18 19 import ( 20 "errors" 21 "fmt" 22 "path/filepath" 23 "strings" 24 25 "github.com/getgauge/gauge-proto/go/gauge_messages" 26 "github.com/getgauge/gauge/config" 27 "github.com/getgauge/gauge/formatter" 28 "github.com/getgauge/gauge/gauge" 29 "github.com/getgauge/gauge/logger" 30 "github.com/getgauge/gauge/parser" 31 "github.com/getgauge/gauge/runner" 32 "github.com/getgauge/gauge/util" 33 ) 34 35 type rephraseRefactorer struct { 36 oldStep *gauge.Step 37 newStep *gauge.Step 38 isConcept bool 39 runner runner.Runner 40 } 41 42 type refactoringResult struct { 43 Success bool 44 SpecsChanged []*gauge_messages.FileChanges 45 ConceptsChanged []*gauge_messages.FileChanges 46 RunnerFilesChanged []*gauge_messages.FileChanges 47 Errors []string 48 Warnings []string 49 } 50 51 func (res *refactoringResult) String() string { 52 o := `Refactoring result from gauge: 53 Specs changed : %s 54 Concepts changed : %s 55 Source files changed : %s 56 Warnings : %s 57 ` 58 return fmt.Sprintf(o, res.specFilesChanged(), res.conceptFilesChanged(), res.runnerFilesChanged(), res.Warnings) 59 } 60 61 func (refactoringResult *refactoringResult) appendWarnings(warnings []*parser.Warning) { 62 if refactoringResult.Warnings == nil { 63 refactoringResult.Warnings = make([]string, 0) 64 } 65 for _, warning := range warnings { 66 refactoringResult.Warnings = append(refactoringResult.Warnings, warning.Message) 67 } 68 } 69 70 func (refactoringResult *refactoringResult) AllFilesChanged() []string { 71 filesChanged := make([]string, 0) 72 filesChanged = append(filesChanged, refactoringResult.specFilesChanged()...) 73 filesChanged = append(filesChanged, refactoringResult.conceptFilesChanged()...) 74 filesChanged = append(filesChanged, refactoringResult.runnerFilesChanged()...) 75 return filesChanged 76 } 77 78 func (refactoringResult *refactoringResult) conceptFilesChanged() []string { 79 filesChanged := make([]string, 0) 80 for _, fileChange := range refactoringResult.ConceptsChanged { 81 filesChanged = append(filesChanged, fileChange.FileName) 82 } 83 return filesChanged 84 } 85 86 func (refactoringResult *refactoringResult) specFilesChanged() []string { 87 filesChanged := make([]string, 0) 88 for _, filesChange := range refactoringResult.SpecsChanged { 89 filesChanged = append(filesChanged, filesChange.FileName) 90 } 91 return filesChanged 92 } 93 94 func (refactoringResult *refactoringResult) runnerFilesChanged() []string { 95 filesChanged := make([]string, 0) 96 for _, fileChange := range refactoringResult.RunnerFilesChanged { 97 filesChanged = append(filesChanged, fileChange.FileName) 98 } 99 return filesChanged 100 } 101 102 func (refactoringResult *refactoringResult) WriteToDisk() { 103 if !refactoringResult.Success { 104 return 105 } 106 // fileChange.FileContent need not be deprecated. To save the refactored file, it is much simpler and less error prone 107 // to replace the file with new content, rather than parsing again and replacing specific lines. 108 for _, fileChange := range refactoringResult.SpecsChanged { 109 util.SaveFile(fileChange.FileName, fileChange.FileContent, true) //nolint:staticcheck 110 } 111 for _, fileChange := range refactoringResult.ConceptsChanged { 112 util.SaveFile(fileChange.FileName, fileChange.FileContent, true) //nolint:staticcheck 113 } 114 } 115 116 // GetRefactoringChanges given an old step and new step gives the list of steps that need to be changed to perform refactoring. 117 // It also provides the changes to be made on the implementation files. 118 func GetRefactoringChanges(oldStep, newStep string, r runner.Runner, specDirs []string, saveToDisk bool) *refactoringResult { 119 if newStep == oldStep { 120 return &refactoringResult{Success: true} 121 } 122 agent, errs := getRefactorAgent(oldStep, newStep, r) 123 124 if len(errs) > 0 { 125 var messages []string 126 for _, err := range errs { 127 messages = append(messages, err.Error()) 128 } 129 return rephraseFailure(messages...) 130 } 131 result, specs, conceptDictionary := parseSpecsAndConcepts(specDirs) 132 if !result.Success { 133 return result 134 } 135 136 refactorResult := agent.getRefactoringChangesFor(specs, conceptDictionary, saveToDisk) 137 refactorResult.Warnings = append(refactorResult.Warnings, result.Warnings...) 138 return refactorResult 139 } 140 141 func parseSpecsAndConcepts(specDirs []string) (*refactoringResult, []*gauge.Specification, *gauge.ConceptDictionary) { 142 result := &refactoringResult{Success: true, Errors: make([]string, 0), Warnings: make([]string, 0)} 143 144 var specs []*gauge.Specification 145 var specParseResults []*parser.ParseResult 146 147 for _, dir := range specDirs { 148 specFiles := util.GetSpecFiles([]string{filepath.Join(config.ProjectRoot, dir)}) 149 specSlice, specParseResultsSlice := parser.ParseSpecFiles(specFiles, &gauge.ConceptDictionary{}, gauge.NewBuildErrors()) 150 specs = append(specs, specSlice...) 151 specParseResults = append(specParseResults, specParseResultsSlice...) 152 } 153 154 addErrorsAndWarningsToRefactoringResult(result, specParseResults...) 155 if !result.Success { 156 return result, nil, nil 157 } 158 159 conceptDictionary, parseResult, err := parser.CreateConceptsDictionary() 160 if err != nil { 161 return rephraseFailure(err.Error()), nil, nil 162 } 163 addErrorsAndWarningsToRefactoringResult(result, parseResult) 164 return result, specs, conceptDictionary 165 } 166 167 func rephraseFailure(errs ...string) *refactoringResult { 168 return &refactoringResult{Success: false, Errors: errs} 169 } 170 171 func addErrorsAndWarningsToRefactoringResult(refactorResult *refactoringResult, parseResults ...*parser.ParseResult) { 172 for _, parseResult := range parseResults { 173 if !parseResult.Ok { 174 refactorResult.Success = false 175 refactorResult.Errors = append(refactorResult.Errors, parseResult.Errors()...) 176 } 177 refactorResult.appendWarnings(parseResult.Warnings) 178 } 179 } 180 181 func (agent *rephraseRefactorer) getRefactoringChangesFor(specs []*gauge.Specification, conceptDictionary *gauge.ConceptDictionary, saveToDisk bool) *refactoringResult { 182 specsRefactored, conceptFilesRefactored := agent.rephraseInSpecsAndConcepts(&specs, conceptDictionary) 183 result := agent.refactorStepImplementations(saveToDisk) 184 if !result.Success { 185 return result 186 } 187 result.SpecsChanged, result.ConceptsChanged = getFileChanges(specs, conceptDictionary, specsRefactored, conceptFilesRefactored) 188 return result 189 } 190 191 func (agent *rephraseRefactorer) refactorStepImplementations(shouldSaveChanges bool) *refactoringResult { 192 result := &refactoringResult{Success: false, Errors: make([]string, 0), Warnings: make([]string, 0)} 193 if !agent.isConcept { 194 stepName, err, warning := agent.getStepNameFromRunner(agent.runner) 195 if err != nil { 196 result.Errors = append(result.Errors, err.Error()) 197 return result 198 } 199 if warning == nil { 200 runnerFilesChanged, err := agent.requestRunnerForRefactoring(agent.runner, stepName, shouldSaveChanges) 201 if err != nil { 202 result.Errors = append(result.Errors, fmt.Sprintf("Cannot perform refactoring: %s", err)) 203 return result 204 } 205 result.RunnerFilesChanged = runnerFilesChanged 206 } else { 207 result.Warnings = append(result.Warnings, warning.Message) 208 } 209 } 210 result.Success = true 211 return result 212 } 213 214 func (agent *rephraseRefactorer) rephraseInSpecsAndConcepts(specs *[]*gauge.Specification, conceptDictionary *gauge.ConceptDictionary) (map[*gauge.Specification][]*gauge.StepDiff, map[string][]*gauge.StepDiff) { 215 specsRefactored := make(map[*gauge.Specification][]*gauge.StepDiff) 216 conceptsRefactored := make(map[string][]*gauge.StepDiff) 217 orderMap := agent.createOrderOfArgs() 218 for _, spec := range *specs { 219 diffs, isRefactored := spec.RenameSteps(agent.oldStep, agent.newStep, orderMap) 220 if isRefactored { 221 specsRefactored[spec] = diffs 222 } 223 } 224 isConcept := false 225 for _, concept := range conceptDictionary.ConceptsMap { 226 isRefactored := false 227 for _, item := range concept.ConceptStep.Items { 228 if item.Kind() == gauge.StepKind { 229 diff, isRefactored := item.(*gauge.Step).Rename(agent.oldStep, agent.newStep, isRefactored, orderMap, &isConcept) 230 if isRefactored { 231 conceptsRefactored[concept.FileName] = append(conceptsRefactored[concept.FileName], diff) 232 } 233 } 234 } 235 } 236 agent.isConcept = isConcept 237 return specsRefactored, conceptsRefactored 238 } 239 240 func (agent *rephraseRefactorer) createOrderOfArgs() map[int]int { 241 orderMap := make(map[int]int, len(agent.newStep.Args)) 242 for i, arg := range agent.newStep.Args { 243 orderMap[i] = SliceIndex(len(agent.oldStep.Args), func(i int) bool { return agent.oldStep.Args[i].String() == arg.String() }) 244 } 245 return orderMap 246 } 247 248 // SliceIndex gives the index of the args. 249 func SliceIndex(limit int, predicate func(i int) bool) int { 250 for i := 0; i < limit; i++ { 251 if predicate(i) { 252 return i 253 } 254 } 255 return -1 256 } 257 258 func getRefactorAgent(oldStepText, newStepText string, r runner.Runner) (*rephraseRefactorer, []parser.ParseError) { 259 specParser := new(parser.SpecParser) 260 stepTokens, errs := specParser.GenerateTokens("* "+oldStepText+"\n"+"*"+newStepText, "") 261 if len(errs) > 0 { 262 return nil, errs 263 } 264 265 steps := make([]*gauge.Step, 0) 266 for _, stepToken := range stepTokens { 267 step, parseRes := parser.CreateStepUsingLookup(stepToken, nil, "") 268 if parseRes != nil && len(parseRes.ParseErrors) > 0 { 269 return nil, parseRes.ParseErrors 270 } 271 steps = append(steps, step) 272 } 273 return &rephraseRefactorer{oldStep: steps[0], newStep: steps[1], runner: r}, []parser.ParseError{} 274 } 275 276 func (agent *rephraseRefactorer) requestRunnerForRefactoring(testRunner runner.Runner, stepName string, shouldSaveChanges bool) ([]*gauge_messages.FileChanges, error) { 277 refactorRequest, err := agent.createRefactorRequest(stepName, shouldSaveChanges) 278 if err != nil { 279 return nil, err 280 } 281 refactorResponse := agent.sendRefactorRequest(testRunner, refactorRequest) 282 var runnerError error 283 if !refactorResponse.GetSuccess() { 284 logger.Errorf(false, "Refactoring error response from runner: %v", refactorResponse.GetError()) 285 runnerError = errors.New(refactorResponse.GetError()) 286 } 287 if len(refactorResponse.GetFileChanges()) == 0 { 288 for _, file := range refactorResponse.GetFilesChanged() { 289 refactorResponse.FileChanges = append(refactorResponse.FileChanges, &gauge_messages.FileChanges{FileName: file}) 290 } 291 } 292 return refactorResponse.GetFileChanges(), runnerError 293 } 294 295 func (agent *rephraseRefactorer) sendRefactorRequest(testRunner runner.Runner, refactorRequest *gauge_messages.Message) *gauge_messages.RefactorResponse { 296 response, err := testRunner.ExecuteMessageWithTimeout(refactorRequest) 297 if err != nil { 298 return &gauge_messages.RefactorResponse{Success: false, Error: err.Error()} 299 } 300 return response.GetRefactorResponse() 301 } 302 303 //Todo: Check for inline tables 304 func (agent *rephraseRefactorer) createRefactorRequest(stepName string, shouldSaveChanges bool) (*gauge_messages.Message, error) { 305 oldStepValue, err := agent.getStepValueFor(agent.oldStep, stepName) 306 if err != nil { 307 return nil, err 308 } 309 orderMap := agent.createOrderOfArgs() 310 newStepName := agent.generateNewStepName(oldStepValue.Args, orderMap) 311 newStepValue, err := parser.ExtractStepValueAndParams(newStepName, false) 312 if err != nil { 313 return nil, err 314 } 315 oldProtoStepValue := gauge.ConvertToProtoStepValue(oldStepValue) 316 newProtoStepValue := gauge.ConvertToProtoStepValue(newStepValue) 317 return &gauge_messages.Message{MessageType: gauge_messages.Message_RefactorRequest, 318 RefactorRequest: &gauge_messages.RefactorRequest{ 319 OldStepValue: oldProtoStepValue, 320 NewStepValue: newProtoStepValue, 321 ParamPositions: agent.createParameterPositions(orderMap), 322 SaveChanges: shouldSaveChanges, 323 }, 324 }, nil 325 } 326 327 func (agent *rephraseRefactorer) generateNewStepName(args []string, orderMap map[int]int) string { 328 agent.newStep.PopulateFragments() 329 paramIndex := 0 330 for _, fragment := range agent.newStep.Fragments { 331 if fragment.GetFragmentType() == gauge_messages.Fragment_Parameter { 332 if orderMap[paramIndex] != -1 { 333 fragment.GetParameter().Value = args[orderMap[paramIndex]] 334 } 335 paramIndex++ 336 } 337 } 338 return parser.ConvertToStepText(agent.newStep.Fragments) 339 } 340 341 func (agent *rephraseRefactorer) getStepNameFromRunner(r runner.Runner) (string, error, *parser.Warning) { 342 stepNameMessage := &gauge_messages.Message{MessageType: gauge_messages.Message_StepNameRequest, StepNameRequest: &gauge_messages.StepNameRequest{StepValue: agent.oldStep.Value}} 343 responseMessage, err := r.ExecuteMessageWithTimeout(stepNameMessage) 344 345 if err != nil { 346 return "", err, nil 347 } 348 if !(responseMessage.GetStepNameResponse().GetIsStepPresent()) { 349 return "", nil, &parser.Warning{Message: fmt.Sprintf("Step implementation not found: %s", agent.oldStep.LineText)} 350 } 351 if responseMessage.GetStepNameResponse().GetHasAlias() { 352 return "", fmt.Errorf("steps with aliases : '%s' cannot be refactored", strings.Join(responseMessage.GetStepNameResponse().GetStepName(), "', '")), nil 353 } 354 if responseMessage.GetStepNameResponse().GetIsExternal() { 355 return "", fmt.Errorf("external step: Cannot refactor '%s' is in external project or library", strings.Join(responseMessage.GetStepNameResponse().GetStepName(), "', '")), nil 356 } 357 return responseMessage.GetStepNameResponse().GetStepName()[0], nil, nil 358 } 359 360 func (agent *rephraseRefactorer) createParameterPositions(orderMap map[int]int) []*gauge_messages.ParameterPosition { 361 paramPositions := make([]*gauge_messages.ParameterPosition, 0) 362 for k, v := range orderMap { 363 paramPositions = append(paramPositions, &gauge_messages.ParameterPosition{NewPosition: int32(k), OldPosition: int32(v)}) 364 } 365 return paramPositions 366 } 367 368 func (agent *rephraseRefactorer) getStepValueFor(step *gauge.Step, stepName string) (*gauge.StepValue, error) { 369 return parser.ExtractStepValueAndParams(stepName, false) 370 } 371 372 func createDiffs(diffs []*gauge.StepDiff) []*gauge_messages.TextDiff { 373 textDiffs := []*gauge_messages.TextDiff{} 374 for _, diff := range diffs { 375 newtext := strings.TrimSpace(formatter.FormatStep(diff.NewStep)) 376 if diff.IsConcept && !diff.OldStep.InConcept() { 377 newtext = strings.Replace(newtext, "*", "#", -1) 378 } 379 oldFragments := util.GetLinesFromText(strings.TrimSpace(formatter.FormatStep(&diff.OldStep))) 380 d := &gauge_messages.TextDiff{ 381 Span: &gauge_messages.Span{ 382 Start: int64(diff.OldStep.LineNo), 383 StartChar: 0, 384 End: int64(diff.OldStep.LineNo + len(oldFragments) - 1), 385 EndChar: int64(len(oldFragments[len(oldFragments)-1])), 386 }, 387 Content: newtext, 388 } 389 textDiffs = append(textDiffs, d) 390 } 391 return textDiffs 392 } 393 394 func getFileChanges(specs []*gauge.Specification, conceptDictionary *gauge.ConceptDictionary, specsRefactored map[*gauge.Specification][]*gauge.StepDiff, conceptsRefactored map[string][]*gauge.StepDiff) ([]*gauge_messages.FileChanges, []*gauge_messages.FileChanges) { 395 specFiles := []*gauge_messages.FileChanges{} 396 conceptFiles := []*gauge_messages.FileChanges{} 397 for _, spec := range specs { 398 if stepDiffs, ok := specsRefactored[spec]; ok { 399 formatted := formatter.FormatSpecification(spec) 400 specFiles = append(specFiles, &gauge_messages.FileChanges{FileName: spec.FileName, FileContent: formatted, Diffs: createDiffs(stepDiffs)}) 401 } 402 } 403 conceptMap := formatter.FormatConcepts(conceptDictionary) 404 for file, diffs := range conceptsRefactored { 405 conceptFiles = append(conceptFiles, &gauge_messages.FileChanges{FileName: file, FileContent: conceptMap[file], Diffs: createDiffs(diffs)}) 406 } 407 return specFiles, conceptFiles 408 }