diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..89fc349 --- /dev/null +++ b/.env.template @@ -0,0 +1,10 @@ +FS_API="https://api.flagship.io" +FS_AUTH_API="https://auth.flagship.io" +REPOSITORY_BRANCH="master" +DIRECTORY="path/to/directory" +REPOSITORY_URL="https://gitlab.com/org/repo" +NB_CODE_LINES_EDGES=1 +FLAGSHIP_CLIENT_ID=FLAGSHIP_MANAGEMENT_API_CLIENT_ID +FLAGSHIP_CLIENT_SECRET=FLAGSHIP_MANAGEMENT_API_CLIENT_SECRET +ACCOUNT_ID=FLAGSHIP_ACCOUNT_ID +ENVIRONMENT_ID=FLAGSHIP_ENVIRONMENT_ID \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8d2333a..9faae13 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ example/src-js .env coverage +codebase-analyzer diff --git a/README.md b/README.md index c5a7fc8..e728424 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ export ENVIRONMENT_ID=FLAGSHIP_ENVIRONMENT_ID export REPOSITORY_URL=https://gitlab.com/org/repo export REPOSITORY_BRANCH=master export DIRECTORY=./ -./code-analyzer +./codebase-analyzer ``` ### With Docker @@ -41,6 +41,7 @@ docker run -v $(pwd)/your_repo:/your_repo -e FLAGSHIP_CLIENT_ID=FLAGSHIP_MANAGEM ``` ### With Homebrew + ```sh export FLAGSHIP_CLIENT_ID=FLAGSHIP_MANAGEMENT_API_CLIENT_ID export FLAGSHIP_CLIENT_SECRET=FLAGSHIP_MANAGEMENT_API_CLIENT_SECRET @@ -57,6 +58,7 @@ codebase-analyzer ``` ### Supported file languages + - .cs .fs - .dart - .go @@ -140,7 +142,7 @@ analyze_flag_references: ACCOUNT_ID: YOUR_ACCOUNT_ID ENVIRONMENT_ID: YOUR_ENVIRONMENT_ID script: - - /root/code-analyser + - /root/codebase-analyser only: - master ``` @@ -159,13 +161,12 @@ This repository needs go v1.13+ to work 2. Create a .env file to customize your environment variable 3. Run `go run *.go` in the example folder to run the code -### Test +### Test ``` make test ``` - ## Contributors - Guillaume Jacquart [@GuillaumeJacquart](https://github.com/guillaumejacquart) @@ -178,23 +179,25 @@ make test [Apache License.](https://github.com/flagship-io/codebase-analyzer/blob/master/LICENSE) ## About Flagship + ​ drawing ​ [Flagship by AB Tasty](https://www.flagship.io/) is a feature flagging platform for modern engineering and product teams. It eliminates the risks of future releases by separating code deployments from these releases :bulb: With Flagship, you have full control over the release process. You can: ​ + - Switch features on or off through remote config. - Automatically roll-out your features gradually to monitor performance and gather feedback from your most relevant users. - Roll back any feature should any issues arise while testing in production. - Segment users by granting access to a feature based on certain user attributes. - Carry out A/B tests by easily assigning feature variations to groups of users. -​ -drawing -​ -Flagship also allows you to choose whatever implementation method works for you from our many available SDKs or directly through a REST API. Additionally, our architecture is based on multi-cloud providers that offer high performance and highly-scalable managed services. -​ -**To learn more:** -​ + ​ + drawing + ​ + Flagship also allows you to choose whatever implementation method works for you from our many available SDKs or directly through a REST API. Additionally, our architecture is based on multi-cloud providers that offer high performance and highly-scalable managed services. + ​ + **To learn more:** + ​ - [Solution overview](https://www.flagship.io/#showvideo) - A 5mn video demo :movie_camera: - [Documentation](https://docs.developers.flagship.io/) - Our dev portal with guides, how tos, API and SDK references - [Sign up for a free trial](https://www.flagship.io/sign-up/) - Create your free account diff --git a/example/src/flutter/sample.dart b/example/src/flutter/SDK_V3/sample.dart similarity index 100% rename from example/src/flutter/sample.dart rename to example/src/flutter/SDK_V3/sample.dart diff --git a/example/src/flutter/SDK_V4/sample.dart b/example/src/flutter/SDK_V4/sample.dart new file mode 100644 index 0000000..87164c0 --- /dev/null +++ b/example/src/flutter/SDK_V4/sample.dart @@ -0,0 +1,18 @@ +import 'package:flagship/flagship.dart'; + +// Step 1 - Start the Flagship sdk with default configuration. +Flagship.start("_ENV_ID_", "_API_KEY_"); + +// Step 2 - Create visitor with context "isVip" : true +var visitor = Flagship.newVisitor(visitorId: "visitorId", hasConsented: true) + .withContext({"isVip": true}).build(); + +// Step 3 - Fetch flags + visitor.fetchFlags().whenComplete(() { + // Step 4 - Get Flag key + var flag = v.getFlag("displayVipFeature"); + // Step 5 - Read Flag value + var value = flag.value(false); + var value1 = v.getFlag("backgroundColor").value("red"); + var value1 = v.getFlag("backgroundSize").value(1); + }); \ No newline at end of file diff --git a/example/src/ios/SDK_V4/sample.swift b/example/src/ios/SDK_V4/sample.swift new file mode 100644 index 0000000..69efc3f --- /dev/null +++ b/example/src/ios/SDK_V4/sample.swift @@ -0,0 +1,30 @@ +import Flagship + +// Step 1 - Start the Flagship sdk with default configuration. +Flagship.sharedInstance.start(envId: "_ENV_ID_", apiKey: "_API_KEY_") + +// Step 2 - Create visitor with context "isVip" : true +let visitor = Flagship.sharedInstance.newVisitor(visitorId: "visitorId", hasConsented: true) + .withContext(context: ["isVip": true]) + .build() + +// Step 3 - Fetch flags +visitor.fetchFlags { + + // Fetch completed + + // Step 4 - Get Flag key + let flag = visitor.getFlag(key: "btnColor") + + // Step 5 - Read Flag value + let value = flag.value(defaultValue: "red") + + let value = visitor.getFlag(key: "displayVipFeature").value(defaultValue: false) + + // Step 4 - Get Flag key + let flag2 = visitor.getFlag(key: "vipFeature") + + // Step 5 - Read Flag value + let value = flag2.value(defaultValue: 16) + +} \ No newline at end of file diff --git a/example/src/java/SDK_V4/sample.kt b/example/src/java/SDK_V4/sample.kt new file mode 100644 index 0000000..6ac5d00 --- /dev/null +++ b/example/src/java/SDK_V4/sample.kt @@ -0,0 +1,7 @@ +val flagRank = visitor.getFlag("btnColor") +val flagRankValue = flagRank.value("red") + +val flagRankValue2 = visitor.getFlag("backgroundSize").value(1) + +val flagRank1 = visitor.getFlag("showBackground") +val flagRankValue = flagRank1.value(true) \ No newline at end of file diff --git a/example/src/js/SDK_V4/sample.js b/example/src/js/SDK_V4/sample.js new file mode 100644 index 0000000..67dfb30 --- /dev/null +++ b/example/src/js/SDK_V4/sample.js @@ -0,0 +1,75 @@ +//start demo +// Usage: node demo/index.js +const express = require("express"); +const { Flagship, HitType, EventCategory } = require("@flagship.io/js-sdk"); + +const app = express(); +app.use(express.json()); + +const visitorId = "visitor-id"; + +// Step 1: Start the Flagship SDK by providing the environment ID and API key +Flagship.start("", "", { + fetchNow: false, +}); + +// Endpoint to get an item +app.get("/item", async (req, res) => { + + // Step 2: Create a new visitor with a visitor ID and consent status + const visitor = Flagship.newVisitor({ + visitorId, + hasConsented: true, + context: { + fs_is_vip: true, + }, + }); + + // Step 3: Fetch the flags for the visitor + await visitor.fetchFlags(); + + // fe:flag:fs_disable_coupon, string + const fsDisableCoupon = visitor.getFlag("fs_disable_coupon"); + + // Step 4: Get the values of the flags for the visitor + const fsEnableDiscount = visitor.getFlag("fs_enable_discount"); + const fsAddToCartBtnColor = visitor.getFlag("fs_add_to_cart_btn_color"); + + const fsEnableDiscountValue = fsEnableDiscount.getValue(false); + const fsAddToCartBtnColorValue = fsAddToCartBtnColor.getValue("blue"); + + res.json({ + item: { + name: "Flagship T-shirt", + price: 20, + }, + fsEnableDiscount: fsEnableDiscountValue, + fsAddToCartBtnColor: fsAddToCartBtnColorValue, + }); +}); + +// Endpoint to add an item to the cart +app.post("/add-to-cart", async (req, res) => { + + const visitor = Flagship.newVisitor({ + visitorId, + hasConsented: true + }); + + // Step 5: Send a hit to track an action + visitor.sendHit({ + type: HitType.EVENT, + category: EventCategory.ACTION_TRACKING, + action: "add-to-cart-clicked", + }); + + res.json(null); +}); + +const port = 3000; + +app.listen(port, () => { + console.log(`Server running on port ${port}`); +}); + +//end demo \ No newline at end of file diff --git a/example/src/js/SDK_V4/sample.ts b/example/src/js/SDK_V4/sample.ts new file mode 100644 index 0000000..76a54da --- /dev/null +++ b/example/src/js/SDK_V4/sample.ts @@ -0,0 +1,29 @@ +import { Flagship } from "@flagship.io/js-sdk"; + +Flagship.start("your_env_id", "your_api_key"); + +const visitor = Flagship.newVisitor({ + visitorId: "your_visitor_id", + context: { isVip: true }, +}); + +visitor.fetchFlags(); + +const variableKey = visitor.getFlag("flagKey"); +const boxSizeFlagDefaultValue = variableKey.getValue("flagDefaultValue"); + +const variableKey1: any = visitor.getFlag("flagKey1"); + +const variableKey3: any = visitor.getFlag("flagKey3"); +const boxSizeFlagDefaultValue1 = variableKey3.getValue(16); + +const variableKey4: any = visitor.getFlag("flagKey4"); + +const variableKey5: any = visitor.getFlag("flagKey5").getValue(false); + +// fe:flag: flagKey6, true +const variable6: any = visitor.getFlag("flagKey6"); + +visitor.getFlag("flagKey5").getValue(false); + +visitor.getFlagValue("FlagKey5", false); diff --git a/example/src/net/SDK_V4/sample.cs b/example/src/net/SDK_V4/sample.cs new file mode 100644 index 0000000..a9eaaec --- /dev/null +++ b/example/src/net/SDK_V4/sample.cs @@ -0,0 +1,24 @@ +using Flagship.Main; + +//Step 2: Create a visitor +var visitor = Fs.NewVisitor("", true) + .SetContext(new Dictionary(){["isVip"] = true}) + .Build(); + + +//Step 3: Fetch flag from the Flagship platform +await visitor.FetchFlags(); + +/* Step 4: Retrieves a flag named "displayVipFeature", + */ +var flag = visitor.GetFlag("showBtn"); + +//Step 5: get the flag value and if the flag does not exist, it returns the default value "false" +var flagValue = flag.GetValue(false); + +var flag_ = visitor.GetFlag("btnSize").GetValue(15); + +var flag1 = visitor.GetFlag("btnColor"); +var flagValue = flag1.GetValue("red"); + +Console.WriteLine($"Flag {flagValue}"); \ No newline at end of file diff --git a/example/src/net/SDK_V4/sample.fs b/example/src/net/SDK_V4/sample.fs new file mode 100644 index 0000000..d0df006 --- /dev/null +++ b/example/src/net/SDK_V4/sample.fs @@ -0,0 +1,20 @@ +open System.Collections.Generic +open Flagship + +let client = FlagshipBuilder.Start("ENV_ID","API_KEY"); + +let context = new Dictionary(); +context.Add("key", "value"); + +let visitor = client.NewVisitor("visitor_id", context); + +visitor.FetchFlags(); + +let btnColorFlag = visitor.GetFlag("btnColor"); +let btnColorFlagValue = btnColorFlag.GetValue('red'); + +let flag = visitor.GetFlag("btnSize"); +let flagValue = flag.GetValue(13); + +let showBtnFlag = visitor.GetFlag("showBtn"); +let showBtnFlagValue = showBtnFlag.GetValue(true); \ No newline at end of file diff --git a/example/src/php/SDK_V4/sample.php b/example/src/php/SDK_V4/sample.php new file mode 100644 index 0000000..05b87bd --- /dev/null +++ b/example/src/php/SDK_V4/sample.php @@ -0,0 +1,25 @@ +use Flagship\Flagship; + +// Step 1: start the SDK +Flagship::start("", ""); + + //Step 2: Create a visitor + $visitor = Flagship::newVisitor("", true) + ->setContext(["isVip" => true]) + ->build(); + + //Step 3: Fetch flag from the Flagship platform + $visitor->fetchFlags(); + + //Step 4: Retrieves a flag named "displayVipFeature" + $flag = $visitor->getFlag("displayVipFeature"); + + //Step 5: Returns the flag value ,or if the flag does not exist, it returns the default value "false" + echo "flag value:". $flag->getValue(false); + + $flag1 = $visitor->getFlag("vipFeatureSize")->getValue(15); + + $flag1 = $visitor->getFlag("vipFeatureColor")->getValue("red"); + + //Step 6: Batch all the collected hits and send them + Flagship::close(); \ No newline at end of file diff --git a/example/src/react/SDK_V3/sample.jsx b/example/src/react/SDK_V3/sample.jsx index f985916..4163471 100644 --- a/example/src/react/SDK_V3/sample.jsx +++ b/example/src/react/SDK_V3/sample.jsx @@ -4,7 +4,7 @@ import {useFsFlag} from "@flagship.io/react-sdk"; export const MyReactComponent = () => { const backgroundColorFlag = useFsFlag("backgroundColor", "green") const btnColorFlag = useFsFlag("btnColor", "red") - const backgroundSize = useFsFlag("backgroundColor", 16) + const backgroundSize = useFsFlag("backgroundSize", 16) const showBtn = useFsFlag("showBtn", true) return ( diff --git a/example/src/react/SDK_V4/sample.jsx b/example/src/react/SDK_V4/sample.jsx new file mode 100644 index 0000000..7ac18b7 --- /dev/null +++ b/example/src/react/SDK_V4/sample.jsx @@ -0,0 +1,28 @@ +import React from "react"; +import { useFsFlag } from "@flagship.io/react-sdk"; + +export const MyReactComponent = () => { + ///Step 2: Retrieves a flag named "backgroundColor" + const flag = useFsFlag("backgroundColor") + + //Step 3: Returns the value of the flag or if the flag does not exist, it returns the default value "green" + const flagValue = flag.getValue("green") + + + const flag_ = useFsFlag("btnSize").getValue(16) + + const flag1 = useFsFlag("showBtn") + const flagValue_ = flag1.getValue(true) + + return ( + + ); +}; \ No newline at end of file diff --git a/internal/api/syncFlags.go b/internal/api/syncFlags.go index 51aae11..264d2a8 100644 --- a/internal/api/syncFlags.go +++ b/internal/api/syncFlags.go @@ -4,7 +4,7 @@ import ( "bytes" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" log "github.com/sirupsen/logrus" @@ -117,7 +117,7 @@ func generateAuthenticationToken(cfg *config.Config) (string, error) { defer resp.Body.Close() var result AuthResponse - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { log.Fatal("Error while reading body", err.Error()) @@ -129,7 +129,7 @@ func generateAuthenticationToken(cfg *config.Config) (string, error) { return result.AccessToken, nil } else { - body, _ := ioutil.ReadAll(resp.Body) + body, _ := io.ReadAll(resp.Body) return "", fmt.Errorf("error when calling Flagship authentication API. Status: %s, body: %s", resp.Status, string(body)) } } @@ -167,7 +167,7 @@ func callAPI(cfg *config.Config, flagInfos FlagUsageRequest) error { } if resp.StatusCode != 201 { - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { log.Fatal("Error while reading body", err.Error()) diff --git a/internal/files/search.go b/internal/files/search.go index ea36ba6..40bb05a 100644 --- a/internal/files/search.go +++ b/internal/files/search.go @@ -3,17 +3,44 @@ package files import ( "encoding/json" "fmt" - "io/ioutil" + "os" "path/filepath" "regexp" "strings" "github.com/flagship-io/codebase-analyzer/internal/model" "github.com/flagship-io/codebase-analyzer/pkg/config" - "github.com/sirupsen/logrus" + log "github.com/sirupsen/logrus" "github.com/thoas/go-funk" ) +type MatchVariableFlag struct { + Variable string + FlagKey string + CodeLines string + CodeLineHighlight int + CodeLineURL string + LineNumber int +} + +type MatchVariableDefaultValue struct { + Variable string + FlagDefaultValue string + FlagType string +} + +type RegexStruct struct { + isFlag bool + isDefaultValue bool + regex []string +} + +type FlagIndexesStruct struct { + isFlag bool + isDefaultValue bool + FlagIndexes [][]int +} + func GetFlagType(defaultValue string) (string, string) { var flagType string = "string" @@ -54,7 +81,7 @@ func GetFlagType(defaultValue string) (string, string) { // SearchFiles search code pattern in files and return results and error func SearchFiles(cfg *config.Config, path string, resultChannel chan model.FileSearchResult) { // Read file contents - fileContent, err := ioutil.ReadFile(path) + fileContent, err := os.ReadFile(path) if err != nil { resultChannel <- model.FileSearchResult{ File: path, @@ -63,18 +90,30 @@ func SearchFiles(cfg *config.Config, path string, resultChannel chan model.FileS } return } + fileContentStr := strings.ReplaceAll(string(fileContent), "\r\n", "\n") // Get file extension to choose matching regex ext := filepath.Ext(path) - var regexes []string + var regexesNotSplit []string + var regexesSplit []RegexStruct for _, extRegex := range model.LanguageRegexes { - regxp := regexp.MustCompile(extRegex.FileExtension) - if regxp.Match([]byte(ext)) { - regexes = append(regexes, extRegex.Regexes...) + if !extRegex.Split { + regxp := regexp.MustCompile(extRegex.FileExtension) + if regxp.Match([]byte(ext)) { + regexesNotSplit = append(regexesNotSplit, extRegex.Regexes...) + } + } + + if extRegex.Split { + regxp := regexp.MustCompile(extRegex.FileExtension) + if regxp.Match([]byte(ext)) { + regexesSplit = append(regexesSplit, RegexStruct{isFlag: extRegex.ForFlag, isDefaultValue: extRegex.ForDefaultValue, regex: extRegex.Regexes}) + } } } - if len(regexes) == 0 { + + if len(regexesNotSplit) == 0 && len(regexesSplit) == 0 { resultChannel <- model.FileSearchResult{ File: path, Results: nil, @@ -84,14 +123,15 @@ func SearchFiles(cfg *config.Config, path string, resultChannel chan model.FileS } // Add default regex for flags in commentaries - regexes = append(regexes, - `fs:flag:(.+)`, + regexesNotSplit = append(regexesNotSplit, + `fe:flag:\s*(\w+)\s*[,]\s*(\w+)\s*`, ) - results := []model.SearchResult{} + resultsNotSplit := []model.SearchResult{} + flagIndexesNotSplit := [][]int{} + flagIndexesSplit := flagIndexSplitter(fileContentStr, path, regexesSplit) - flagIndexes := [][]int{} - for _, regex := range regexes { + for _, regex := range regexesNotSplit { regxp := regexp.MustCompile(regex) flagLineIndexes := regxp.FindAllStringIndex(fileContentStr, -1) @@ -102,24 +142,24 @@ func SearchFiles(cfg *config.Config, path string, resultChannel chan model.FileS for _, submatchIndex := range submatchIndexes { if len(submatchIndex) < 3 { - logrus.WithFields(logrus.Fields{ + log.WithFields(log.Fields{ "reason": fmt.Sprintf("Did not find the flag key in file %s. Code: %s", path, submatch), }).Error("Key not found") continue } if len(submatchIndex) < 6 { - logrus.WithFields(logrus.Fields{ + log.WithFields(log.Fields{ "reason": fmt.Sprintf("Did not find the flag default value in file %s. Code: %s", path, submatch), }).Warn("Type unknown") - flagIndexes = append(flagIndexes, []int{ + flagIndexesNotSplit = append(flagIndexesNotSplit, []int{ flagLineIndex[0] + submatchIndex[2], flagLineIndex[0] + submatchIndex[3], }) continue } - flagIndexes = append(flagIndexes, []int{ + flagIndexesNotSplit = append(flagIndexesNotSplit, []int{ flagLineIndex[0] + submatchIndex[2], flagLineIndex[0] + submatchIndex[3], flagLineIndex[0] + submatchIndex[4], @@ -129,7 +169,60 @@ func SearchFiles(cfg *config.Config, path string, resultChannel chan model.FileS } } - for _, flagIndex := range flagIndexes { + var flagMatches []MatchVariableFlag + var defaultValueMatches []MatchVariableDefaultValue + + for _, flagIndexes := range flagIndexesSplit { + for _, flagIndex := range flagIndexes.FlagIndexes { + variable := fileContentStr[flagIndex[0]:flagIndex[1]] + if len(flagIndex) < 3 { + log.WithFields(log.Fields{ + "reason": fmt.Sprintf("Did not find the flag key or default value in file %s", path), + }).Error("Key not found") + continue + } + + keyOrDefaultValue := fileContentStr[flagIndex[2]:flagIndex[3]] + + if flagIndexes.isFlag { + firstLineIndex := getSurroundingLineIndex(fileContentStr, flagIndex[0], true, cfg.NbLineCodeEdges) + lastLineIndex := getSurroundingLineIndex(fileContentStr, flagIndex[1], false, cfg.NbLineCodeEdges) + code := fileContentStr[firstLineIndex:lastLineIndex] + keyWrapped := keyWrapper(keyOrDefaultValue, fileContentStr, flagIndex) + lineNumber := getLineFromPos(fileContentStr, flagIndex[2]) + codeLineHighlight := getLineFromPos(code, strings.Index(code, keyWrapped)) + _ = codeLineHighlight + + flagMatches = append(flagMatches, MatchVariableFlag{ + Variable: variable, + FlagKey: keyOrDefaultValue, + CodeLines: code, + CodeLineHighlight: codeLineHighlight, + CodeLineURL: getCodeURL(cfg, path, &lineNumber), + LineNumber: lineNumber, + }) + } + + if flagIndexes.isDefaultValue { + flagType, defaultValue_ := GetFlagType(keyOrDefaultValue) + + if variable == "" || keyOrDefaultValue == "" || defaultValue_ == "" { + flagType = "unknown" + } + + if variable != "" { + defaultValueMatches = append(defaultValueMatches, MatchVariableDefaultValue{ + Variable: variable, + FlagDefaultValue: keyOrDefaultValue, + FlagType: flagType, + }) + } + + } + } + } + + for _, flagIndex := range flagIndexesNotSplit { // Extract the code with a certain number of lines defaultValue_ := "" flagType := "unknown" @@ -157,23 +250,30 @@ func SearchFiles(cfg *config.Config, path string, resultChannel chan model.FileS lineNumber := getLineFromPos(fileContentStr, flagIndex[0]) codeLineHighlight := getLineFromPos(code, strings.Index(code, keyWrapper)) - results = append(results, model.SearchResult{ - FlagKey: key, - FlagDefaultValue: defaultValue_, - FlagType: flagType, - CodeLines: code, - CodeLineHighlight: codeLineHighlight, - CodeLineURL: getCodeURL(cfg, path, &lineNumber), - // Get line number of the code - LineNumber: lineNumber, - }) + if key != "" { + resultsNotSplit = append(resultsNotSplit, model.SearchResult{ + FlagKey: key, + FlagDefaultValue: defaultValue_, + FlagType: flagType, + CodeLines: code, + CodeLineHighlight: codeLineHighlight, + CodeLineURL: getCodeURL(cfg, path, &lineNumber), + LineNumber: lineNumber, + }) + } } + resultsSplit := matchFlagWithDefaultValue(flagMatches, defaultValueMatches) + combinedResults := append(resultsNotSplit, resultsSplit...) + + res, _ := json.Marshal(combinedResults) + fmt.Println(string(res)) + resultChannel <- model.FileSearchResult{ File: path, FileURL: getCodeURL(cfg, path, nil), - Results: results, + Results: combinedResults, Error: err, } } @@ -230,3 +330,96 @@ func getLineFromPos(input string, indexPosition int) int { } return lineNumber } + +func keyWrapper(key, fileContentStr string, flagIndex []int) string { + keyWrapper := key + nbCharsWrapping := 5 + if flagIndex[2] > nbCharsWrapping && flagIndex[3] < len(fileContentStr)-nbCharsWrapping { + keyWrapper = fileContentStr[flagIndex[2]-nbCharsWrapping : flagIndex[3]+nbCharsWrapping] + } + return keyWrapper +} + +func flagIndexSplitter(fileContentStr, path string, regexesSplit []RegexStruct) []FlagIndexesStruct { + flagIndexesSplits := []FlagIndexesStruct{} + for _, regexSplit := range regexesSplit { + for _, regex := range regexSplit.regex { + var indexSplit [][]int + regxp := regexp.MustCompile(regex) + flagLineIndexes := regxp.FindAllStringIndex(fileContentStr, -1) + for _, flagLineIndex := range flagLineIndexes { + submatch := fileContentStr[flagLineIndex[0]:flagLineIndex[1]] + submatchIndexes := regxp.FindAllStringSubmatchIndex(submatch, -1) + + for _, submatchIndex := range submatchIndexes { + if len(submatchIndex) < 3 { + log.WithFields(log.Fields{ + "reason": fmt.Sprintf("Did not find the variable in file %s. Code: %s", path, submatch), + }).Error("Key not found") + continue + } + + if len(submatchIndex) < 6 { + log.WithFields(log.Fields{ + "reason": fmt.Sprintf("Did not find the flag key or default value in file %s. Code: %s", path, submatch), + }).Warn("Type unknown") + indexSplit = append(indexSplit, []int{ + flagLineIndex[0] + submatchIndex[2], + flagLineIndex[0] + submatchIndex[3], + }) + continue + } + + indexSplit = append(indexSplit, []int{ + flagLineIndex[0] + submatchIndex[2], + flagLineIndex[0] + submatchIndex[3], + flagLineIndex[0] + submatchIndex[4], + flagLineIndex[0] + submatchIndex[5], + }) + } + } + + flagIndexesSplits = append(flagIndexesSplits, FlagIndexesStruct{ + isFlag: regexSplit.isFlag, + isDefaultValue: regexSplit.isDefaultValue, + FlagIndexes: indexSplit, + }) + } + } + + return flagIndexesSplits +} + +func matchFlagWithDefaultValue(flagMatches []MatchVariableFlag, defaultValueMatches []MatchVariableDefaultValue) []model.SearchResult { + nameMap := make(map[string]MatchVariableDefaultValue) + results := []model.SearchResult{} + + for _, p2 := range defaultValueMatches { + nameMap[p2.Variable] = p2 + } + + for _, p1 := range flagMatches { + + searchResult := model.SearchResult{ + FlagKey: p1.FlagKey, + FlagDefaultValue: "", + FlagType: "unknown", + CodeLines: p1.CodeLines, + CodeLineHighlight: p1.CodeLineHighlight, + CodeLineURL: p1.CodeLineURL, + LineNumber: p1.LineNumber, + } + + if p2, found := nameMap[p1.Variable]; found { + searchResult.FlagType = p2.FlagType + searchResult.FlagDefaultValue = p2.FlagDefaultValue + } + + if p1.FlagKey != "" { + results = append(results, searchResult) + } + + } + + return results +} diff --git a/internal/files/search_test.go b/internal/files/search_test.go index e0ac215..78d3ee6 100644 --- a/internal/files/search_test.go +++ b/internal/files/search_test.go @@ -73,6 +73,14 @@ func TestSearchFiles(t *testing.T) { {name: "vipFeature", lineNumber: 11, codeLineHighlight: 6}, }, }, + { + filePath: "../../example/src/ios/SDK_V4/sample.swift", + flags: []flag{ + {name: "displayVipFeature", lineNumber: 22, codeLineHighlight: 6}, + {name: "btnColor", lineNumber: 17, codeLineHighlight: 6}, + {name: "vipFeature", lineNumber: 25, codeLineHighlight: 6}, + }, + }, { filePath: "../../example/src/java/SDK_V2/sample.java", flags: []flag{ @@ -107,6 +115,14 @@ func TestSearchFiles(t *testing.T) { {name: "showBackground", lineNumber: 12, codeLineHighlight: 6}, }, }, + { + filePath: "../../example/src/java/SDK_V4/sample.kt", + flags: []flag{ + {name: "backgroundSize", lineNumber: 4, codeLineHighlight: 4}, + {name: "btnColor", lineNumber: 1, codeLineHighlight: 1}, + {name: "showBackground", lineNumber: 6, codeLineHighlight: 6}, + }, + }, { filePath: "../../example/src/js/SDK_V2/sample.js", flags: []flag{ @@ -175,6 +191,14 @@ func TestSearchFiles(t *testing.T) { {name: "showBtn", lineNumber: 17, codeLineHighlight: 6}, }, }, + { + filePath: "../../example/src/net/SDK_V4/sample.cs", + flags: []flag{ + {name: "btnSize", lineNumber: 19, codeLineHighlight: 6}, + {name: "showBtn", lineNumber: 14, codeLineHighlight: 6}, + {name: "btnColor", lineNumber: 21, codeLineHighlight: 6}, + }, + }, { filePath: "../../example/src/net/SDK_V1/sample.vb", flags: []flag{ @@ -212,10 +236,18 @@ func TestSearchFiles(t *testing.T) { flags: []flag{ {name: "backgroundColor", lineNumber: 5, codeLineHighlight: 5}, {name: "btnColor", lineNumber: 6, codeLineHighlight: 6}, - {name: "backgroundColor", lineNumber: 7, codeLineHighlight: 6}, + {name: "backgroundSize", lineNumber: 7, codeLineHighlight: 6}, {name: "showBtn", lineNumber: 8, codeLineHighlight: 6}, }, }, + { + filePath: "../../example/src/react/SDK_V4/sample.jsx", + flags: []flag{ + {name: "btnSize", lineNumber: 12, codeLineHighlight: 6}, + {name: "backgroundColor", lineNumber: 6, codeLineHighlight: 6}, + {name: "showBtn", lineNumber: 14, codeLineHighlight: 6}, + }, + }, { filePath: "../../example/src/php/SDK_V1/sample.php", flags: []flag{ @@ -241,13 +273,29 @@ func TestSearchFiles(t *testing.T) { }, }, { - filePath: "../../example/src/flutter/sample.dart", + filePath: "../../example/src/php/SDK_V4/sample.php", + flags: []flag{ + {name: "vipFeatureSize", lineNumber: 20, codeLineHighlight: 6}, + {name: "vipFeatureColor", lineNumber: 22, codeLineHighlight: 6}, + {name: "displayVipFeature", lineNumber: 15, codeLineHighlight: 6}, + }, + }, + { + filePath: "../../example/src/flutter/SDK_V3/sample.dart", flags: []flag{ {name: "displayVipFeature", lineNumber: 15, codeLineHighlight: 6}, {name: "backgroundColor", lineNumber: 16, codeLineHighlight: 6}, {name: "backgroundSize", lineNumber: 17, codeLineHighlight: 6}, }, }, + { + filePath: "../../example/src/flutter/SDK_V4/sample.dart", + flags: []flag{ + {name: "backgroundColor", lineNumber: 16, codeLineHighlight: 6}, + {name: "backgroundSize", lineNumber: 17, codeLineHighlight: 6}, + {name: "displayVipFeature", lineNumber: 13, codeLineHighlight: 6}, + }, + }, } resultChannel := make(chan model.FileSearchResult) diff --git a/internal/model/language_regexes.go b/internal/model/language_regexes.go index 0742fbe..2e39ac2 100644 --- a/internal/model/language_regexes.go +++ b/internal/model/language_regexes.go @@ -6,8 +6,11 @@ import ( ) type LanguageRegex struct { - FileExtension string `json:"file_extension"` - Regexes []string `json:"regexes"` + FileExtension string `json:"file_extension"` + Regexes []string `json:"regexes"` + Split bool `json:"split"` + ForFlag bool `json:"search_flag"` + ForDefaultValue bool `json:"search_default_value"` } var LanguageRegexes = []LanguageRegex{ @@ -15,11 +18,29 @@ var LanguageRegexes = []LanguageRegex{ FileExtension: `\.[jt]sx?$`, Regexes: []string{ `useFsFlag[(](?:(?:\s*['"](.*)['"]\s*,\s*(["'].*\s*[^"]*["']|[^)]*))\s*[)])?`, // SDK React V3 + `useFsFlag[(](?:(?:\s*["'](.*)["']\s*[)]\s*.getValue[(](["'].*\s*[^"]*["']|[^)]*))\s*[)])?`, // SDK React V4 `['"]?key['"]?\s*\:\s*['"](.+?)['"](?:.*\s*)['"]?defaultValue['"]?\s*\:\s*(['"].*['"]|[^\r\n\t\f\v,}]+).*[},]?`, // SDK JS V2 && SDK React V2 `getFlag[(](?:(?:\s*["'](.*)["']\s*,\s*(["'].*\s*[^"]*["']|[^)]*))\s*[)])?`, // SDK JS V3 + `getFlag[(](?:(?:\s*["'](\w*)["']\s*[)]\s*.getValue[(](["']\w*\s*[^"]*["']|[^)]*))\s*[)])?`, // SDK JS V4 }, }, - + { + FileExtension: `\.[jt]sx?$`, + Regexes: []string{ + `(?:(\w+)\s*[?]?\s*[:]?\s*(?:number|string|boolean|any|void| never|null|undefined|bigint|symbol|object|IFSFlag|FSFlag)?)\s*[=]\s*(?:.*)useFsFlag[(](?:(?:\s*["'](\w*)["']\s*[)]\s*))[^\.]`, // SDK React V4 Key + `(?:(\w+)\s*[?]?\s*[:]?\s*(?:number|string|boolean|any|void| never|null|undefined|bigint|symbol|object|IFSFlag|FSFlag)?)\s*[=]\s*(?:.*)getFlag[(](?:(?:\s*["'](\w*)["']\s*[)]\s*))[^\.]`, // SDK JS V4 Key + }, + Split: true, + ForFlag: true, + }, + { + FileExtension: `\.[jt]sx?$`, + Regexes: []string{ + `\s*(\w*)[\.]getValue[(](["']?\w*["']?)[)]`, // SDK JS & React V4 Default value + }, + Split: true, + ForDefaultValue: true, + }, { FileExtension: `\.go$`, Regexes: []string{ @@ -39,27 +60,78 @@ var LanguageRegexes = []LanguageRegex{ `\.getFlag[(](?:\s*(?:\s*["'](.*)["']\s*,\s*(["'].*\s*[^"]*["']|[^)]*))\s*[)])?`, // SDK JAVA V3 }, }, + { + FileExtension: `\.kt$`, + Regexes: []string{ + `\.getModification\(\s*(?:\s*["']([\w\-]+)['"]\s*,\s*(["'][^"]*['"]|[+-]?(?:\d*[.])?\d+|true|false|False|True)(?:\s*,\s*(?:true|false|\d+|"[^"]*"))?\s*\))?`, // SDK ANDROID V2 + `\.getFlag[(](?:\s*(?:\s*["'](.*)["']\s*,\s*(["'].*\s*[^"]*["']|[^)]*))\s*[)])?`, // SDK ANDROID V3 + `getFlag[(](?:(?:\s*["'](\w*)["']\s*[)]\s*.value[(](["'].*\s*[^"]*["']|[^)]*))\s*[)])?`, // SDK ANDROID V4 + + }, + }, + { + FileExtension: `\.kt$`, + Regexes: []string{ + `(?:(\w+))\s*[=]\s*(?:.*)getFlag[(](?:(?:\s*["'](\w*)["']\s*[)]\s*))[^\.]`, // SDK ANDROID V4 Key + }, + Split: true, + ForFlag: true, + }, + { + FileExtension: `\.kt$`, + Regexes: []string{ + `\s*(\w*)[\.]value[(](["']?\w*["']?)[)]`, // SDK ANDROID V4 Default value + }, + Split: true, + ForDefaultValue: true, + }, { FileExtension: `\.php$`, Regexes: []string{ `\-\>getModification\(\s*(?:\s*["']([\w\-]+)['"]\s*,\s*(["'][^"]*['"]|[+-]?(?:\d*[.])?\d+|true|false|False|True)(?:\s*,\s*(?:true|false|\d+|"[^"]*"))?\s*\))?`, // SDK PHP V1 && SDK PHP V2 `\-\>getFlag[(](?:\s*(?:\s*["'](.*)["']\s*,\s*(["'].*\s*[^"]*["']|[^)]*))\s*[)])?`, // SDK PHP V3 + `\-\>getFlag[(](?:\s*(?:\s*["'](\w*)["']\s*[)]\s*\-\>\s*getValue[(](["'].*\s*[^"]*["']|[^)]*))\s*[)])?`, // SDK PHP V4 }, }, { - FileExtension: `\.kt$`, + FileExtension: `\.php$`, Regexes: []string{ - `\.getModification\(\s*(?:\s*["']([\w\-]+)['"]\s*,\s*(["'][^"]*['"]|[+-]?(?:\d*[.])?\d+|true|false|False|True)(?:\s*,\s*(?:true|false|\d+|"[^"]*"))?\s*\))?`, // SDK ANDROID V2 - `\.getFlag[(](?:\s*(?:\s*["'](.*)["']\s*,\s*(["'].*\s*[^"]*["']|[^)]*))\s*[)])?`, // SDK ANDROID V3 - + `(?:(\w+))\s*[=]\s*(?:.*)getFlag[(](?:(?:\s*["'](\w*)["']\s*[)]\s*))[^\-]`, // SDK PHP V4 Key }, + Split: true, + ForFlag: true, + }, + { + FileExtension: `\.php$`, + Regexes: []string{ + `\s*(\w*)\s*\-\>\s*getValue[(](["']?\w*["']?)[)]`, // SDK PHP V4 Default value + }, + Split: true, + ForDefaultValue: true, }, { FileExtension: `\.swift$`, Regexes: []string{ `\.getModification\((?:\s*["'](\w+)['"]\s*,\s*default(?:String|Double|Float|Int|Bool|Json|Array)\s*:\s*(["'][^"]*['"]|[+-]?(?:\d*[.])?\d+|true|false|False|True)\s*(?:,\s*activate\s*:\s*(?:true|false|\d+|"[^"]*"))?\s*\))?`, // SDK iOS V2 `\.getFlag[(](?:\s*key\s*:\s*(?:\s*["'](.*)["']\s*,\s*defaultValue\s*:\s*(["'].*\s*[^"]*["']|[^)]*))\s*[)])?`, // SDK iOS V3 + `getFlag[(](?:\s*key\s*[:]\s*(?:\s*["'](\w*)["']\s*[)]\s*\.value[(]\s*defaultValue\s*[:]\s*(["'].*\s*[^"]*["']|[^)]*))\s*[)])?`, // SDK iOS V4 + }, + }, + { + FileExtension: `\.swift$`, + Regexes: []string{ + `(?:(\w+))\s*[=]\s*(?:.*)getFlag[(](?:(?:\s*key\s*[:]\s*["'](\w*)["']\s*[)]\s*))[^\.]`, // SDK iOS V4 Key }, + Split: true, + ForFlag: true, + }, + { + FileExtension: `\.swift$`, + Regexes: []string{ + `\s*(\w*)[\.]value[(]\s*defaultValue\s*[:]\s*(["']?\w*["']?)[)]`, // SDK iOS V4 Default value + }, + Split: true, + ForDefaultValue: true, }, { FileExtension: `\.m$`, @@ -73,20 +145,55 @@ var LanguageRegexes = []LanguageRegex{ Regexes: []string{ `\.GetModification\((?:\s*["']([\w\-]+)['"]\s*,\s*(["'][^"]*['"]|[+-]?(?:\d*[.])?\d+|true|false|False|True)(?:\s*,\s*(?:true|false|\d+|"[^"]*"))?\s*\))?`, // SDK .NET V1 `\.GetFlag\((?:\s*["']([\w\-]+)['"]\s*,\s*(["'][^"]*['"]|[+-]?(?:\d*[.])?\d+|true|false|False|True)(?:\s*,\s*(?:true|false|\d+|"[^"]*"))?\s*\))?`, // SDK .NET V3 + `GetFlag[(](?:(?:\s*["'](\w*)["']\s*[)]\s*.GetValue[(](["'].*\s*[^"]*["']|[^)]*))\s*[)])?`, // SDK .NET V4 + }, + }, + { + FileExtension: `\.[fc]s$`, + Regexes: []string{ + `(?:(\w+))\s*[=]\s*(?:.*)GetFlag[(](?:(?:\s*["'](\w*)["']\s*[)]\s*))[^\.]`, // SDK .NET V4 Key }, + Split: true, + ForFlag: true, + }, + { + FileExtension: `\.[fc]s$`, + Regexes: []string{ + `\s*(\w*)[\.]GetValue[(](["']?\w*["']?)[)]`, // SDK .NET V4 Default value + }, + Split: true, + ForDefaultValue: true, }, { FileExtension: `\.vb$`, Regexes: []string{ `\.GetModification\((?:\s*["']([\w\-]+)['"]\s*,\s*(["'][^"]*['"]|[+-]?(?:\d*[.])?\d+|true|false|False|True)(?:\s*,\s*(?:true|True|false|False|\d+|"[^"]*"))?\s*\))?`, // SDK .NET V1 `\.GetFlag\((?:\s*["']([\w\-]+)['"]\s*,\s*(["'][^"]*['"]|[+-]?(?:\d*[.])?\d+|true|false|False|True)(?:\s*,\s*(?:true|false|\d+|"[^"]*"))?\s*\))?`, // SDK .NET V3 + `GetFlag[(](?:(?:\s*["'](\w*)["']\s*[)]\s*.GetValue[(](["'].*\s*[^"]*["']|[^)]*))\s*[)])?`, // SDK .NET V4 + }, + }, + { + FileExtension: `\.dart$`, + Regexes: []string{ + `getFlag[(](?:(?:\s*["'](.*)["']\s*,\s*(["'].*\s*[^"]*["']|[^)]*))\s*[)])?`, // SDK Flutter V1-V2-V3 + `getFlag[(](?:(?:\s*["'](\w*)["']\s*[)]\s*.value[(](["'].*\s*[^"]*["']|[^)]*))\s*[)])?`, // SDK Flutter V4 + }, + }, + { + FileExtension: `\.dart$`, + Regexes: []string{ + `(?:(\w+))\s*[=]\s*(?:.*)getFlag[(](?:(?:\s*["'](\w*)["']\s*[)]\s*))[^\.]`, // SDK Flutter V4 Key }, + Split: true, + ForFlag: true, }, { FileExtension: `\.dart$`, Regexes: []string{ - `getFlag[(](?:(?:\s*["'](.*)["']\s*,\s*(["'].*\s*[^"]*["']|[^)]*))\s*[)])?`, // SDK Flutter V1-V2-V3 + `\s*(\w*)[\.]value[(](["']?\w*["']?)[)]`, // SDK Flutter V4 Default value }, + Split: true, + ForDefaultValue: true, }, }