diff --git a/internal/languages/java/detectors/.snapshots/TestJavaString-string b/internal/languages/java/detectors/.snapshots/TestJavaString-string index bc4c22bf9..cedcbab2c 100644 --- a/internal/languages/java/detectors/.snapshots/TestJavaString-string +++ b/internal/languages/java/detectors/.snapshots/TestJavaString-string @@ -363,6 +363,21 @@ children: data: value: Hello World isliteral: true +- node: 44 + content: s += "!!" + data: + value: Hello World!!! + isliteral: true +- node: 57 + content: s2 += args[0] + data: + value: hey * + isliteral: false +- node: 67 + content: s2 += " there" + data: + value: hey * there + isliteral: false - node: 38 content: Greeting + "!" data: diff --git a/internal/languages/javascript/detectors/.snapshots/TestJavascriptStringDetector-string_assign_eq b/internal/languages/javascript/detectors/.snapshots/TestJavascriptStringDetector-string_assign_eq index e9818bd76..46d3f41ba 100644 --- a/internal/languages/javascript/detectors/.snapshots/TestJavascriptStringDetector-string_assign_eq +++ b/internal/languages/javascript/detectors/.snapshots/TestJavascriptStringDetector-string_assign_eq @@ -181,6 +181,21 @@ children: id: 36 range: 6:8 - 6:9 +- node: 11 + content: x += "b" + data: + value: ab + isliteral: true +- node: 19 + content: x += name + data: + value: ab* + isliteral: false +- node: 30 + content: y += "c" + data: + value: '*c' + isliteral: false - node: 6 content: '"a"' data: diff --git a/internal/languages/php/analyzer/analyzer.go b/internal/languages/php/analyzer/analyzer.go index 13eeb2e68..6669c5056 100644 --- a/internal/languages/php/analyzer/analyzer.go +++ b/internal/languages/php/analyzer/analyzer.go @@ -21,12 +21,7 @@ func New(builder *tree.Builder) language.Analyzer { func (analyzer *analyzer) Analyze(node *sitter.Node, visitChildren func() error) error { switch node.Type() { - case "declaration_list", - "class_declaration", - "method_declaration", - "anonymous_function_creation_expression", - "for_statement", - "block": + case "declaration_list", "class_declaration", "anonymous_function_creation_expression", "for_statement", "block", "method_declaration": return analyzer.withScope(language.NewScope(analyzer.scope), func() error { return visitChildren() }) @@ -50,7 +45,19 @@ func (analyzer *analyzer) Analyze(node *sitter.Node, visitChildren func() error) return analyzer.analyzeGenericConstruct(node, visitChildren) case "switch_label": return visitChildren() - case "binary_expression", "unary_op_expression", "argument", "encapsed_string", "sequence_expression": + case "dynamic_variable_name": + return analyzer.analyzeDynamicVariableName(node, visitChildren) + case "binary_expression", + "unary_op_expression", + "argument", + "encapsed_string", + "sequence_expression", + "array_element_initializer", + "formal_parameters", + "include_expression", + "include_once_expression", + "require_expression", + "require_once_expression": return analyzer.analyzeGenericOperation(node, visitChildren) case "while_statement", "do_statement", "if_statement", "expression_statement", "compound_statement": // statements don't have results return visitChildren() @@ -126,9 +133,11 @@ func (analyzer *analyzer) analyzeConditional(node *sitter.Node, visitChildren fu return visitChildren() } +// foo(1, 2); // foo->bar(1, 2); func (analyzer *analyzer) analyzeMethodInvocation(node *sitter.Node, visitChildren func() error) error { - analyzer.lookupVariable(node.ChildByFieldName("object")) + analyzer.lookupVariable(node.ChildByFieldName("object")) // method + analyzer.lookupVariable(node.ChildByFieldName("function")) // function if arguments := node.ChildByFieldName("arguments"); arguments != nil { analyzer.builder.Dataflow(node, arguments) @@ -152,6 +161,7 @@ func (analyzer *analyzer) analyzeFieldAccess(node *sitter.Node, visitChildren fu func (analyzer *analyzer) analyzeParameter(node *sitter.Node, visitChildren func() error) error { name := node.ChildByFieldName("name") analyzer.builder.Alias(node, name) + analyzer.scope.Declare(analyzer.builder.ContentFor(name), name) return visitChildren() } @@ -162,6 +172,12 @@ func (analyzer *analyzer) analyzeSwitch(node *sitter.Node, visitChildren func() return visitChildren() } +func (analyzer *analyzer) analyzeDynamicVariableName(node *sitter.Node, visitChildren func() error) error { + analyzer.lookupVariable(node.NamedChild(0)) + + return visitChildren() +} + // default analysis, where the children are assumed to be aliases func (analyzer *analyzer) analyzeGenericConstruct(node *sitter.Node, visitChildren func() error) error { analyzer.builder.Alias(node, analyzer.builder.ChildrenFor(node)...) diff --git a/internal/languages/php/detectors/.snapshots/TestPHPString-string b/internal/languages/php/detectors/.snapshots/TestPHPString-string index d8cd6c926..720960508 100644 --- a/internal/languages/php/detectors/.snapshots/TestPHPString-string +++ b/internal/languages/php/detectors/.snapshots/TestPHPString-string @@ -1,10 +1,10 @@ type: program id: 0 -range: 1:1 - 15:3 +range: 1:1 - 18:1 dataflow_sources: - 1 - 2 - - 100 + - 117 children: - type: php_tag id: 1 @@ -12,7 +12,7 @@ children: content: "' - id: 101 - range: 15:1 - 15:3 + id: 118 + range: 17:1 - 17:3 +- node: 12 + content: '"Hello World"' + data: + value: Hello World + isliteral: true - node: 14 content: Hello World data: value: Hello World isliteral: true +- node: 52 + content: $s .= "!!" + data: + value: '*!!!' + isliteral: false +- node: 74 + content: $s2 .= $args[0] + data: + value: hey * + isliteral: false +- node: 88 + content: $s2 .= " there" + data: + value: hey * there + isliteral: false - node: 39 content: self::Greeting . "!" data: - value: '**' + value: '*!' + isliteral: false +- node: 57 + content: '"!!"' + data: + value: '!!' + isliteral: true +- node: 68 + content: '"hey "' + data: + value: 'hey ' + isliteral: true +- node: 93 + content: '" there"' + data: + value: ' there' + isliteral: true +- node: 104 + content: '"foo ''{$s2}'' bar"' + data: + value: foo 'hey * there' bar isliteral: false +- node: 46 + content: '"!"' + data: + value: '!' + isliteral: true - node: 59 content: '!!' data: @@ -472,6 +591,16 @@ children: data: value: ' there' isliteral: true +- node: 106 + content: foo ' + data: + value: foo ' + isliteral: true +- node: 112 + content: ''' bar' + data: + value: ''' bar' + isliteral: true - node: 48 content: '!' data: diff --git a/internal/languages/php/detectors/string/string.go b/internal/languages/php/detectors/string/string.go index 1b77938ab..1993b87dd 100644 --- a/internal/languages/php/detectors/string/string.go +++ b/internal/languages/php/detectors/string/string.go @@ -28,10 +28,17 @@ func (detector *stringDetector) DetectAt( ) ([]interface{}, error) { switch node.Type() { case "string": + value := node.Content() + if node.Parent() != nil && node.Parent().Type() != "encapsed_string" { + value = stringutil.StripQuotes(value) + } + return []interface{}{common.String{ - Value: stringutil.StripQuotes(node.Content()), + Value: value, IsLiteral: true, }}, nil + case "encapsed_string": + return common.ConcatenateChildStrings(node, detectorContext) case "binary_expression": if node.Children()[1].Content() == "." { return common.ConcatenateChildStrings(node, detectorContext) diff --git a/internal/languages/php/detectors/testdata/string.php b/internal/languages/php/detectors/testdata/string.php index 6ddb251ac..395d88ac2 100644 --- a/internal/languages/php/detectors/testdata/string.php +++ b/internal/languages/php/detectors/testdata/string.php @@ -10,6 +10,8 @@ public static function main($args) $s2 = "hey "; $s2 .= $args[0]; $s2 .= " there"; + + $s3 = "foo '{$s2}' bar"; } } -?> \ No newline at end of file +?> diff --git a/internal/languages/php/pattern/pattern.go b/internal/languages/php/pattern/pattern.go index e06a50998..14d0d95be 100644 --- a/internal/languages/php/pattern/pattern.go +++ b/internal/languages/php/pattern/pattern.go @@ -13,9 +13,11 @@ import ( var ( // $ or $ or $ - patternQueryVariableRegex = regexp.MustCompile(`\$<(?P[^>:!\.]+)(?::(?P[^>]+))?>`) - matchNodeRegex = regexp.MustCompile(`\$`) - ellipsisRegex = regexp.MustCompile(`\$<\.\.\.>`) + patternQueryVariableRegex = regexp.MustCompile(`\$<(?P[^>:!\.]+)(?::(?P[^>]+))?>`) + matchNodeRegex = regexp.MustCompile(`\$`) + ellipsisRegex = regexp.MustCompile(`\$<\.\.\.>`) + unanchoredPatternNodeTypes = []string{} + patternMatchNodeContainerTypes = []string{"formal_parameters", "simple_parameter", "argument"} allowedPatternQueryTypes = []string{"_"} ) @@ -36,6 +38,14 @@ func (*Pattern) FixupMissing(node *tree.Node) string { return ";" } +func (*Pattern) FixupVariableDummyValue(input []byte, node *tree.Node, dummyValue string) string { + if slices.Contains([]string{"named_type"}, node.Parent().Type()) { + return "$" + dummyValue + } + + return dummyValue +} + func (*Pattern) ExtractVariables(input string) (string, []language.PatternVariable, error) { nameIndex := patternQueryVariableRegex.SubexpIndex("name") typesIndex := patternQueryVariableRegex.SubexpIndex("types") @@ -87,8 +97,21 @@ func (*Pattern) FindUnanchoredPoints(input []byte) [][]int { return ellipsisRegex.FindAllIndex(input, -1) } +func (*Pattern) IsLeaf(node *tree.Node) bool { + // Encapsed string literal + switch node.Type() { + case "encapsed_string": + namedChildren := node.NamedChildren() + if len(namedChildren) == 1 && namedChildren[0].Type() == "string" { + return true + } + } + return false +} + func (*Pattern) LeafContentTypes() []string { return []string{ + "encapsed_string", "string", "name", "integer", @@ -97,19 +120,44 @@ func (*Pattern) LeafContentTypes() []string { } } -// ToDo: func (*Pattern) IsAnchored(node *tree.Node) (bool, bool) { + if slices.Contains(unanchoredPatternNodeTypes, node.Type()) { + return false, false + } + parent := node.Parent() if parent == nil { return true, true } - // Class body class_body - // function block - // lambda () -> {} block - // try {} catch () {} - // unAnchored := []string{"class_body", "block", "try_statement", "catch_type", "resource_specification"} - unAnchored := []string{""} + if parent.Type() == "method_declaration" { + // visibility + if node == parent.ChildByFieldName("name") { + return false, true + } + + // type + if node == parent.ChildByFieldName("parameters") { + return true, false + } + + return false, false + } + + // Associative array elements are unanchored + // eg. array("foo" => 42) + if parent.Type() == "array_creation_expression" && + node.Type() == "array_element_initializer" && + len(node.NamedChildren()) == 2 { + return false, false + } + + // Class body declaration_list + // function/block compound_statement + unAnchored := []string{ + "declaration_list", + "compound_statement", + } isUnanchored := !slices.Contains(unAnchored, parent.Type()) return isUnanchored, isUnanchored @@ -119,6 +167,31 @@ func (*Pattern) IsRoot(node *tree.Node) bool { return !slices.Contains([]string{"expression_statement", "php_tag", "program"}, node.Type()) && !node.IsMissing() } -func (*Pattern) NodeTypes(node *tree.Node) []string { +func (patternLanguage *Pattern) NodeTypes(node *tree.Node) []string { + parent := node.Parent() + if parent == nil { + return []string{node.Type()} + } + + if (node.Type() == "string" && parent.Type() != "encapsed_string") || + (node.Type() == "encapsed_string" && patternLanguage.IsLeaf(node)) { + return []string{"encapsed_string", "string"} + } + return []string{node.Type()} } + +func (*Pattern) TranslateContent(fromNodeType, toNodeType, content string) string { + if fromNodeType == "string" && toNodeType == "encapsed_string" { + return fmt.Sprintf(`"%s"`, content[1:len(content)-1]) + } + if fromNodeType == "encapsed_string" && toNodeType == "string" { + return fmt.Sprintf("'%s'", content[1:len(content)-1]) + } + + return content +} + +func (*Pattern) ContainerTypes() []string { + return patternMatchNodeContainerTypes +} diff --git a/internal/languages/ruby/detectors/.snapshots/TestRubyStringDetector-string_assign_eq b/internal/languages/ruby/detectors/.snapshots/TestRubyStringDetector-string_assign_eq index f4346c113..da9671ae1 100644 --- a/internal/languages/ruby/detectors/.snapshots/TestRubyStringDetector-string_assign_eq +++ b/internal/languages/ruby/detectors/.snapshots/TestRubyStringDetector-string_assign_eq @@ -149,6 +149,21 @@ children: id: 29 range: 6:8 - 6:9 +- node: 8 + content: x += "b" + data: + value: ab + isliteral: true +- node: 15 + content: x += name + data: + value: ab* + isliteral: false +- node: 23 + content: y += "c" + data: + value: '*c' + isliteral: false - node: 4 content: '"a"' data: diff --git a/internal/scanner/detectors/common/string.go b/internal/scanner/detectors/common/string.go index 442256ba8..1b00c83e9 100644 --- a/internal/scanner/detectors/common/string.go +++ b/internal/scanner/detectors/common/string.go @@ -1,8 +1,6 @@ package common import ( - "fmt" - "github.com/bearer/bearer/internal/scanner/ast/traversalstrategy" "github.com/bearer/bearer/internal/scanner/ast/tree" "github.com/bearer/bearer/internal/scanner/ruleset" @@ -77,15 +75,7 @@ func ConcatenateChildStrings(node *tree.Node, detectorContext types.Context) ([] } func ConcatenateAssignEquals(node *tree.Node, detectorContext types.Context) ([]interface{}, error) { - dataflowSources := node.ChildByFieldName("left").DataflowSources() - if len(dataflowSources) == 0 { - return nil, nil - } - if len(dataflowSources) != 1 { - return nil, fmt.Errorf("expected exactly one data source for `+=` node but got %d", len(dataflowSources)) - } - - left, leftIsLiteral, err := GetStringValue(dataflowSources[0], detectorContext) + left, leftIsLiteral, err := GetStringValue(node.ChildByFieldName("left"), detectorContext) if err != nil { return nil, err } diff --git a/internal/scanner/detectors/customrule/patternquery/builder/builder.go b/internal/scanner/detectors/customrule/patternquery/builder/builder.go index 102e6eba4..7d674eeb6 100644 --- a/internal/scanner/detectors/customrule/patternquery/builder/builder.go +++ b/internal/scanner/detectors/customrule/patternquery/builder/builder.go @@ -12,6 +12,7 @@ import ( "github.com/bearer/bearer/internal/parser/nodeid" "github.com/bearer/bearer/internal/scanner/ast" asttree "github.com/bearer/bearer/internal/scanner/ast/tree" + "github.com/bearer/bearer/internal/scanner/detectors/customrule/patternquery/builder/bytereplacer" "github.com/bearer/bearer/internal/scanner/language" ) @@ -57,16 +58,30 @@ func Build( return nil, err } - if fixedInput, fixed := fixupInput( + fixupResult, err := fixupInput( patternLanguage, processedInput, inputParams.Variables, tree.RootNode(), - ); fixed { - tree, err = ast.Parse(context.TODO(), language, fixedInput) + ) + if err != nil { + return nil, err + } + + if fixupResult.Changed() { + if log.Trace().Enabled() { + log.Trace().Msgf("fixedInput -> %s", fixupResult.Value()) + } + + tree, err = ast.Parse(context.TODO(), language, fixupResult.Value()) if err != nil { return nil, err } + + inputParams.MatchNodeOffset = fixupResult.Translate(inputParams.MatchNodeOffset) + for i := range inputParams.UnanchoredOffsets { + inputParams.UnanchoredOffsets[i] = fixupResult.Translate(inputParams.UnanchoredOffsets[i]) + } } root := tree.RootNode() @@ -118,13 +133,9 @@ func fixupInput( byteInput []byte, variables []language.PatternVariable, rootNode *asttree.Node, -) ([]byte, bool) { +) (*bytereplacer.Result, error) { + replacer := bytereplacer.New(byteInput) insideError := false - inputOffset := 0 - - newInput := make([]byte, len(byteInput)) - copy(newInput, byteInput) - fixed := false err := rootNode.Walk(func(node *asttree.Node, visitChildren func() error) error { oldInsideError := insideError @@ -141,7 +152,6 @@ func fixupInput( } var newValue string - var originalValue string if insideError { variable := getVariableFor(node, patternLanguage, variables) @@ -158,7 +168,6 @@ func fixupInput( return nil } variable.DummyValue = newValue - originalValue = variable.DummyValue } else { if log.Trace().Enabled() { log.Trace().Msgf("attempting pattern fixup (missing node). node: %s", node.Debug()) @@ -170,42 +179,10 @@ func fixupInput( } } - fixed = true - valueOffset := len(newValue) - len(originalValue) - - prefix := newInput[:node.ContentStart.Byte+inputOffset] - suffix := newInput[node.ContentEnd.Byte+inputOffset:] - // FIXME: We need to append suffix before - // newInput seems to be sharing memory in some circumstances - // suffix before and after the first append are not equal - appendedInput := appendByte([]byte(newValue), suffix...) - newInput = appendByte(prefix, appendedInput...) - - inputOffset += valueOffset - - return nil + return replacer.Replace(node.ContentStart.Byte, node.ContentEnd.Byte, []byte(newValue)) }) - // walk errors are only ones we produce, and we don't make any - if err != nil { - panic(err) - } - - return newInput, fixed -} - -func appendByte(slice []byte, data ...byte) []byte { - m := len(slice) - n := m + len(data) - if n > cap(slice) { // if necessary, reallocate - // allocate double what's needed, for future growth. - newSlice := make([]byte, (n+1)*2) - copy(newSlice, slice) - slice = newSlice - } - slice = slice[0:n] - copy(slice[m:n], data) - return slice + return replacer.Done(), err } func (builder *builder) build(rootNode *asttree.Node) (*Result, error) { @@ -257,7 +234,7 @@ func (builder *builder) compileNode(node *asttree.Node, isRoot bool, isLastChild builder.compileVariableNode(variable) } else if !node.IsNamed() { builder.compileAnonymousNode(node) - } else if len(node.NamedChildren()) == 0 { + } else if len(node.NamedChildren()) == 0 || builder.patternLanguage.IsLeaf(node) { builder.compileLeafNode(node) } else if err := builder.compileNodeWithChildren(node); err != nil { return err @@ -400,8 +377,12 @@ func getVariableFor( patternLanguage language.Pattern, variables []language.PatternVariable, ) *language.PatternVariable { + if slices.Contains(patternLanguage.ContainerTypes(), node.Type()) { + return nil + } + for i, variable := range variables { - if (len(node.NamedChildren()) == 0 || patternLanguage.IsLeaf(node)) && node.Content() == variable.DummyValue { + if node.Content() == variable.DummyValue { return &variables[i] } } diff --git a/internal/scanner/detectors/customrule/patternquery/builder/bytereplacer/bytereplacer.go b/internal/scanner/detectors/customrule/patternquery/builder/bytereplacer/bytereplacer.go new file mode 100644 index 000000000..e949124a4 --- /dev/null +++ b/internal/scanner/detectors/customrule/patternquery/builder/bytereplacer/bytereplacer.go @@ -0,0 +1,91 @@ +package bytereplacer + +import ( + "bytes" + "fmt" +) + +type Replacer struct { + offsetDelta + result *Result +} + +type offsetDelta struct { + originalOffset, + delta int +} + +type Result struct { + value []byte + offsetDeltas []offsetDelta + changed bool +} + +func New(original []byte) *Replacer { + value := make([]byte, len(original)) + copy(value, original) + + return &Replacer{result: &Result{value: value}} +} + +func (replacer *Replacer) Replace(originalStart, originalEnd int, newValue []byte) error { + if originalStart < replacer.originalOffset { + return fmt.Errorf( + "replacements must be made in sequential order. last offset %d, replacement start %d", + replacer.originalOffset, + originalStart, + ) + } + + if bytes.Equal(replacer.result.value[originalStart:originalEnd], newValue) { + return nil + } + + replacer.result.changed = true + + currentLength := len(replacer.result.value) + suffix := replacer.result.value[replacer.delta+originalEnd : currentLength : currentLength] + + replacer.result.value = append( + replacer.result.value[:replacer.delta+originalStart], + append(newValue, suffix...)..., + ) + + delta := len(newValue) - originalEnd + originalStart + + replacer.originalOffset = originalEnd + replacer.delta += delta + + replacer.result.offsetDeltas = append(replacer.result.offsetDeltas, offsetDelta{ + originalOffset: originalEnd, + delta: delta, + }) + + return nil +} + +func (replacer *Replacer) Done() *Result { + return replacer.result +} + +func (result *Result) Changed() bool { + return result.changed +} + +func (result *Result) Value() []byte { + return result.value +} + +func (result *Result) Translate(originalOffset int) int { + delta := 0 + + for _, offsetDelta := range result.offsetDeltas { + if offsetDelta.originalOffset > originalOffset { + break + } + + delta += offsetDelta.delta + } + + return delta + originalOffset +} diff --git a/internal/scanner/detectors/customrule/patternquery/builder/bytereplacer/bytereplacer_suite_test.go b/internal/scanner/detectors/customrule/patternquery/builder/bytereplacer/bytereplacer_suite_test.go new file mode 100644 index 000000000..76e366da4 --- /dev/null +++ b/internal/scanner/detectors/customrule/patternquery/builder/bytereplacer/bytereplacer_suite_test.go @@ -0,0 +1,13 @@ +package bytereplacer_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestFilters(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "ByteReplacer Suite") +} diff --git a/internal/scanner/detectors/customrule/patternquery/builder/bytereplacer/bytereplacer_test.go b/internal/scanner/detectors/customrule/patternquery/builder/bytereplacer/bytereplacer_test.go new file mode 100644 index 000000000..a5bf9190c --- /dev/null +++ b/internal/scanner/detectors/customrule/patternquery/builder/bytereplacer/bytereplacer_test.go @@ -0,0 +1,138 @@ +package bytereplacer_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/bearer/bearer/internal/scanner/detectors/customrule/patternquery/builder/bytereplacer" +) + +var _ = Describe("Replacer", func() { + var replacer *bytereplacer.Replacer + + BeforeEach(func(ctx SpecContext) { + replacer = bytereplacer.New([]byte("hello world")) + }) + + Describe("Replace", func() { + When("replacements are made in sequential order", func() { + BeforeEach(func(ctx SpecContext) { + Expect(replacer.Replace(0, 1, nil)).To(Succeed()) + }) + + It("does not fail", func(ctx SpecContext) { + Expect(replacer.Replace(1, 2, []byte("foo"))).To(Succeed()) + }) + }) + + When("replacements are made out of order", func() { + BeforeEach(func(ctx SpecContext) { + Expect(replacer.Replace(0, 2, nil)).To(Succeed()) + }) + + It("returns an error", func(ctx SpecContext) { + Expect(replacer.Replace(1, 2, []byte("foo"))).To( + MatchError(ContainSubstring("replacements must be made in sequential order")), + ) + }) + }) + }) +}) + +var _ = Describe("Result", func() { + var replacer *bytereplacer.Replacer + original := []byte("hello world") + + BeforeEach(func(ctx SpecContext) { + replacer = bytereplacer.New(original) + }) + + When("no replacements are made", func() { + var result *bytereplacer.Result + + BeforeEach(func(ctx SpecContext) { + result = replacer.Done() + }) + + Describe("Changed", func() { + It("returns false", func(ctx SpecContext) { + Expect(result.Changed()).To(BeFalse()) + }) + }) + + Describe("Value", func() { + It("returns the original value", func(ctx SpecContext) { + Expect(result.Value()).To(Equal(original)) + }) + }) + + Describe("Translate", func() { + It("returns the original offset", func(ctx SpecContext) { + Expect(result.Translate(0)).To(Equal(0)) + Expect(result.Translate(5)).To(Equal(5)) + Expect(result.Translate(10)).To(Equal(10)) + }) + }) + }) + + When("noop replacements are made", func() { + var result *bytereplacer.Result + + BeforeEach(func(ctx SpecContext) { + replacer.Replace(0, 5, []byte("hello")) // nolint:errcheck + replacer.Replace(6, 6, nil) // nolint:errcheck + result = replacer.Done() + }) + + Describe("Changed", func() { + It("returns false", func(ctx SpecContext) { + Expect(result.Changed()).To(BeFalse()) + }) + }) + + Describe("Value", func() { + It("returns the original value", func(ctx SpecContext) { + Expect(result.Value()).To(Equal(original)) + }) + }) + + Describe("Translate", func() { + It("returns the original offset", func(ctx SpecContext) { + Expect(result.Translate(0)).To(Equal(0)) + Expect(result.Translate(5)).To(Equal(5)) + Expect(result.Translate(10)).To(Equal(10)) + }) + }) + }) + + When("replacements are made", func() { + var result *bytereplacer.Result + + BeforeEach(func(ctx SpecContext) { + replacer.Replace(0, 5, []byte("hi")) // nolint:errcheck + replacer.Replace(5, 5, []byte("!")) // nolint:errcheck + replacer.Replace(6, 11, []byte("testing123")) // nolint:errcheck + result = replacer.Done() + }) + + Describe("Changed", func() { + It("returns true", func(ctx SpecContext) { + Expect(result.Changed()).To(BeTrue()) + }) + }) + + Describe("Value", func() { + It("returns the updated value", func(ctx SpecContext) { + Expect(result.Value()).To(Equal([]byte("hi! testing123"))) + }) + }) + + Describe("Translate", func() { + It("returns the new offset", func(ctx SpecContext) { + Expect(result.Translate(0)).To(Equal(0)) + Expect(result.Translate(5)).To(Equal(3)) + Expect(result.Translate(11)).To(Equal(14)) + }) + }) + }) +}) diff --git a/internal/scanner/languagescanner/languagescanner.go b/internal/scanner/languagescanner/languagescanner.go index 574d36776..3f41cce99 100644 --- a/internal/scanner/languagescanner/languagescanner.go +++ b/internal/scanner/languagescanner/languagescanner.go @@ -93,7 +93,7 @@ func (scanner *Scanner) Scan( } if log.Trace().Enabled() { - log.Trace().Msgf("tree (%d nodes):\n%s", tree.NodeCount(), tree.RootNode().SitterNode().String()) + log.Trace().Msgf("tree (%d nodes):\n%s", tree.NodeCount(), tree.RootNode().Dump()) } sharedCache := cache.NewShared(scanner.ruleSet.Rules()) diff --git a/internal/scanner/variableshape/variableshape.go b/internal/scanner/variableshape/variableshape.go index c8eb722dc..d47cfba6f 100644 --- a/internal/scanner/variableshape/variableshape.go +++ b/internal/scanner/variableshape/variableshape.go @@ -143,7 +143,7 @@ func NewSet(language language.Language, ruleSet *ruleset.Set) (*Set, error) { for _, rule := range ruleSet.Rules() { if err := set.add(language, rule); err != nil { - return nil, err + return nil, fmt.Errorf("error adding %s: %w", rule.ID(), err) } }