Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENG-2320] Reduce cyclomatic complexity of the browse function #119

Open
wants to merge 10 commits into
base: master
Choose a base branch
from

Conversation

kanapuli
Copy link
Contributor

@kanapuli kanapuli commented Jan 30, 2025

Description

The browse function is too long to understand and there are a lot of branch conditions which has an higher cyclomatic complexity

Current cyclomatic complexity score after refactoring the browse function = 15

Previous cyclomatic complexity score for the browse function = 51

Summary by CodeRabbit

  • New Features

    • Enhanced node attribute handling in OPC UA plugin.
    • Improved error processing for node attributes.
    • Added structured attribute management with new processing functions.
  • Refactor

    • Simplified node path construction logic.
    • Streamlined node definition initialization.
    • Abstracted node attribute processing into separate functions.
  • Bug Fixes

    • Updated error message for nil node class attributes.

@kanapuli kanapuli self-assigned this Jan 30, 2025
Copy link
Contributor

coderabbitai bot commented Jan 30, 2025

Walkthrough

The pull request introduces significant refactoring to the OPC UA plugin's node browsing functionality, specifically in browse.go and node_attributes.go. The changes focus on improving error handling, attribute processing, and code organization for node attributes. A new processNodeAttributes function is introduced to centralize the logic for handling node attributes like NodeClass, BrowseName, Description, AccessLevel, and DataType. The modifications aim to enhance code readability, maintainability, and provide more structured error handling for OPC UA node browsing.

Changes

File Changes
opcua_plugin/browse.go - Refactored newPath construction logic
- Simplified NodeDef initialization
- Added processNodeAttributes function call
opcua_plugin/node_attributes.go - Added AttributeHandler struct
- Introduced new attribute handling functions
- Created dataTypeMap and getDataTypeString function
opcua_plugin/opcua_unittest_test.go - Updated error message assertion for node class nil scenario

Assessment against linked issues

Objective Addressed Explanation
Control Goroutine Creation [ENG-2320] No changes related to Goroutine management were made in this PR.

Possibly related issues

Possibly related PRs

Suggested reviewers

  • JeremyTheocharis

Poem

🐰 In the realm of OPC UA's dance,
Code hops with grace, takes a new stance.
Attributes flow, errors align,
Refactored paths now brightly shine.
A rabbit's code, both clean and bright! 🌟

✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@kanapuli kanapuli marked this pull request as ready for review January 30, 2025 13:18
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
opcua_plugin/node_attributes.go (4)

11-19: Consider making usage examples or docstrings clearer.
The AttributeHandler struct is well laid out, but it might be helpful to include usage comments or docstrings in its fields to guide future maintainers and reduce confusion about how each flag (e.g., affectsNodeClass) should be used in context.


21-59: Consider returning user-friendly errors or status.
handleAttributeStatus returns an unwrapped attr.Status or a generic error message in some cases. Consider returning more contextual error messages if attr.Status is not ua.StatusOK to provide additional information, especially for debugging or logging.


79-161: Consider simplifying high-level flow in processNodeAttributes.
While the function is clear in purpose, the repetitive pattern of calling handleAttributeStatus for each attribute could be further streamlined using shared logic. If performance isn’t the highest priority here, a small refactor or a loop-based approach might reduce boilerplate.


163-183: Add coverage for unknown DataType cases.
The fallback conversion in getDataTypeString returns a string with ns=0 and the raw typeID. Consider logging or returning a specific string to highlight unknown types, so downstream processes can differentiate truly unknown types from ones that map to standard primitives.

opcua_plugin/browse.go (1)

137-138: Potential micro-optimization.
Rather than manually checking path != "" and concatenating with a dot, you could reuse the existing join function further below for path assembly, keeping path concatenation consistent and reducing possible logic duplication.

- newPath = path + "." + newPath
+ newPath = join(path, newPath)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4e410e6 and be42331.

📒 Files selected for processing (3)
  • opcua_plugin/browse.go (1 hunks)
  • opcua_plugin/node_attributes.go (1 hunks)
  • opcua_plugin/opcua_unittest_test.go (1 hunks)
🔇 Additional comments (5)
opcua_plugin/node_attributes.go (1)

68-77: Review forced NodeClass assignment on non-readable attributes.
When attr.Status is ua.StatusBadNotReadable and handler.affectsNodeClass is true, the code forcibly sets the NodeClass to ua.NodeClassObject. This might unintentionally mask the node’s actual class. Ensure that this behavior aligns with your domain requirements.

opcua_plugin/browse.go (3)

136-136: Good use of sanitize for path construction.
This ensures invalid characters in the browseName.Name are replaced, preventing potential naming or path issues elsewhere.


142-144: Struct field assignments are clear and concise.
Initializing NodeDef fields early in one place promotes clarity, making the code easier to follow and reducing the risk of missing an assignment.


147-148: Ensure processNodeAttributes handles misaligned attributes array.
Though you check the number of attributes elsewhere, confirm in your tests that unexpected array lengths (e.g., fewer or extra attributes) don’t cause index out-of-bounds errors or silent failures.

opcua_plugin/opcua_unittest_test.go (1)

118-118: Updated test error message aligns with new error handling logic.
The new message “attribute value is nil for NodeClass” is clearer and more specific, reflecting the revised code’s behavior. Ensure any other tests referencing this message are updated accordingly.

@kanapuli kanapuli changed the title [ENG-2320] Simplify browse function by extracting Attributes code [ENG-2320] Reduce cyclomatic complexity of the browse function Jan 30, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
opcua_plugin/node_attributes.go (4)

11-19: Consider enhancing field documentation.

While the struct is well-documented, consider adding type information to the field comments for better clarity. For example:

// handleOK is the function that handles the OK status
// Type: func(value *ua.Variant) error

21-58: Improve error messages and code structure in handleAttributeStatus.

Consider these improvements:

  1. Use more descriptive error messages by including the attribute name in all error cases
  2. Implement early returns to reduce nesting and improve readability
 func handleAttributeStatus(
     name string,
     attr *ua.DataValue,
     def *NodeDef,
     path string,
     logger Logger,
     handler AttributeHandler,
 ) error {
     if attr == nil {
-        return fmt.Errorf("attribute is nil")
+        return fmt.Errorf("attribute %s is nil", name)
     }

     if attr.Status == ua.StatusOK {
         if attr.Value == nil && handler.requiresValue {
             return fmt.Errorf("attribute value is nil for %s", name)
         }
         return handleOKStatus(attr.Value, handler)
     }

     if attr.Status == ua.StatusBadSecurityModeInsufficient {
-        return errors.New("insufficient security mode")
+        return fmt.Errorf("insufficient security mode for attribute %s", name)
     }

     if attr.Status == ua.StatusBadAttributeIDInvalid {
         if handler.ignoreInvalidAttr {
             return nil
         }
-        return fmt.Errorf("invalid attribute ID")
+        return fmt.Errorf("invalid attribute ID for %s", name)
     }

     if attr.Status == ua.StatusBadNotReadable {
         handleNotReadable(def, handler, path, logger)
         return nil
     }

     return fmt.Errorf("unexpected status %v for attribute %s", attr.Status, name)

158-178: Consider enhancing dataTypeMap coverage.

  1. Consider adding support for additional OPC UA data types:

    • Int64/UInt64
    • ByteString
    • XmlElement
    • LocalizedText
    • QualifiedName
  2. Add validation for the fallback format in getDataTypeString:

 func getDataTypeString(typeID uint32) string {
     if dtype, ok := dataTypeMap[typeID]; ok {
         return dtype
     }
+    if typeID == 0 {
+        return "unknown"
+    }
     return fmt.Sprintf("ns=%d;i=%d", 0, typeID)
 }

1-178: Well-structured extraction of node attribute handling.

The code successfully achieves its goal of reducing cyclomatic complexity by extracting node attribute handling into a separate, well-organized module. The implementation follows Go best practices and provides comprehensive error handling.

The AttributeHandler pattern provides good flexibility and separation of concerns, making the code more maintainable and testable.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be42331 and 6c3eb0a.

📒 Files selected for processing (1)
  • opcua_plugin/node_attributes.go (1 hunks)
🔇 Additional comments (1)
opcua_plugin/node_attributes.go (1)

1-9: LGTM! Clean package structure and imports.

The package name and imports are well-organized and follow Go conventions.

opcua_plugin/node_attributes.go Outdated Show resolved Hide resolved
opcua_plugin/node_attributes.go Outdated Show resolved Hide resolved
if handler.handleNotReadable != nil {
handler.handleNotReadable()
}
logger.Warnf("Access denied for node: %s, continuing...\n", path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

be careful about warning, they will prevent deployment in the management console

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing code in master branch uses logger.Warnf for this scenario and the status quo is maintained.

return nil
},
requiresValue: true,
affectsNodeClass: true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe explain for those edge cases with a short comment why it requires a value and it affects the node class. for nodeclass, quite obvious, for description maybe not that obvious

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments are added now

@JeremyTheocharis
Copy link
Member

Also checkout htis nitpick comment from coderabbit. might make debugging in the future easier:

21-58: Improve error messages and code structure in handleAttributeStatus.

Consider these improvements:

Use more descriptive error messages by including the attribute name in all error cases
Implement early returns to reduce nesting and improve readability
 func handleAttributeStatus(
     name string,
     attr *ua.DataValue,
     def *NodeDef,
     path string,
     logger Logger,
     handler AttributeHandler,
 ) error {
     if attr == nil {
-        return fmt.Errorf("attribute is nil")
+        return fmt.Errorf("attribute %s is nil", name)
     }

     if attr.Status == ua.StatusOK {
         if attr.Value == nil && handler.requiresValue {
             return fmt.Errorf("attribute value is nil for %s", name)
         }
         return handleOKStatus(attr.Value, handler)
     }

     if attr.Status == ua.StatusBadSecurityModeInsufficient {
-        return errors.New("insufficient security mode")
+        return fmt.Errorf("insufficient security mode for attribute %s", name)
     }

     if attr.Status == ua.StatusBadAttributeIDInvalid {
         if handler.ignoreInvalidAttr {
             return nil
         }
-        return fmt.Errorf("invalid attribute ID")
+        return fmt.Errorf("invalid attribute ID for %s", name)
     }

     if attr.Status == ua.StatusBadNotReadable {
         handleNotReadable(def, handler, path, logger)
         return nil
     }

     return fmt.Errorf("unexpected status %v for attribute %s", attr.Status, name)

@kanapuli
Copy link
Contributor Author

Also checkout htis nitpick comment from coderabbit. might make debugging in the future easier:

21-58: Improve error messages and code structure in handleAttributeStatus.

Consider these improvements:

Use more descriptive error messages by including the attribute name in all error cases
Implement early returns to reduce nesting and improve readability
 func handleAttributeStatus(
     name string,
     attr *ua.DataValue,
     def *NodeDef,
     path string,
     logger Logger,
     handler AttributeHandler,
 ) error {
     if attr == nil {
-        return fmt.Errorf("attribute is nil")
+        return fmt.Errorf("attribute %s is nil", name)
     }

     if attr.Status == ua.StatusOK {
         if attr.Value == nil && handler.requiresValue {
             return fmt.Errorf("attribute value is nil for %s", name)
         }
         return handleOKStatus(attr.Value, handler)
     }

     if attr.Status == ua.StatusBadSecurityModeInsufficient {
-        return errors.New("insufficient security mode")
+        return fmt.Errorf("insufficient security mode for attribute %s", name)
     }

     if attr.Status == ua.StatusBadAttributeIDInvalid {
         if handler.ignoreInvalidAttr {
             return nil
         }
-        return fmt.Errorf("invalid attribute ID")
+        return fmt.Errorf("invalid attribute ID for %s", name)
     }

     if attr.Status == ua.StatusBadNotReadable {
         handleNotReadable(def, handler, path, logger)
         return nil
     }

     return fmt.Errorf("unexpected status %v for attribute %s", attr.Status, name)

Addressed

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
opcua_plugin/node_attributes.go (2)

20-57: Consider using constants and early returns for better readability.

The error handling is thorough, but the code could be more maintainable with these improvements:

+// OPC UA status constants for better readability
+const (
+    statusOK = ua.StatusOK
+    statusBadSecurityMode = ua.StatusBadSecurityModeInsufficient
+    statusBadAttributeID = ua.StatusBadAttributeIDInvalid
+    statusBadNotReadable = ua.StatusBadNotReadable
+)

 func handleAttributeStatus(
     name string,
     attr *ua.DataValue,
     def *NodeDef,
     path string,
     logger Logger,
     handler AttributeHandler,
 ) error {
     if attr == nil {
         return fmt.Errorf("attribute is nil for node: %s and attribute: %s", def.NodeID, name)
     }
 
-    if attr.Status == ua.StatusOK {
-        if attr.Value == nil && handler.requiresValue {
-            return fmt.Errorf("attribute value is nil for node: %s and attribute: %s", def.NodeID, name)
-        }
-        return handleOKStatus(attr.Value, handler)
+    switch attr.Status {
+    case statusOK:
+        if attr.Value == nil && handler.requiresValue {
+            return fmt.Errorf("attribute value is nil for node: %s and attribute: %s", def.NodeID, name)
+        }
+        return handleOKStatus(attr.Value, handler)
+    case statusBadSecurityMode:
+        return fmt.Errorf("got insufficient security mode for node: %s and attribute: %s", def.NodeID, name)
+    case statusBadAttributeID:
+        if handler.ignoreInvalidAttr {
+            return nil
+        }
+        return fmt.Errorf("invalid attribute ID for node: %s and attribute: %s", def.NodeID, name)
+    case statusBadNotReadable:
+        handleNotReadable(def, handler, path, logger)
+        return nil
     }
-
-    if attr.Status == ua.StatusBadSecurityModeInsufficient {
-        return fmt.Errorf("got insufficient security mode for node: %s and attribute: %s", def.NodeID, name)
-    }
-
-    if attr.Status == ua.StatusBadAttributeIDInvalid {
-        if handler.ignoreInvalidAttr {
-            return nil
-        }
-        return fmt.Errorf("invalid attribute ID for node: %s and attribute: %s", def.NodeID, name)
-    }
-
-    if attr.Status == ua.StatusBadNotReadable {
-        handleNotReadable(def, handler, path, logger)
-        return nil
-    }
 
     return attr.Status
 }

157-177: Add documentation for the default format.

Consider adding a comment explaining the format used when a type ID is not found in the map.

 func getDataTypeString(typeID uint32) string {
     if dtype, ok := dataTypeMap[typeID]; ok {
         return dtype
     }
+    // Return the OPC UA node ID format (ns=0;i=<typeID>) for unknown types
     return fmt.Sprintf("ns=%d;i=%d", 0, typeID)
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c3eb0a and c962124.

📒 Files selected for processing (2)
  • opcua_plugin/node_attributes.go (1 hunks)
  • opcua_plugin/opcua_unittest_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • opcua_plugin/opcua_unittest_test.go
🔇 Additional comments (5)
opcua_plugin/node_attributes.go (5)

10-18: Well-structured AttributeHandler definition!

The struct is well-documented with clear field purposes and follows idiomatic Go naming conventions.


59-65: Clean and focused implementation!

The function has appropriate nil checks and follows the single responsibility principle.


67-76: Implementation aligns with existing patterns!

The warning log message is maintained as per the discussion in previous reviews, keeping consistency with the master branch.


78-155: ⚠️ Potential issue

Add safety checks and constants for attribute indices.

The function needs validation and better maintainability:

+// Node attribute indices for better maintainability
+const (
+    nodeClassIdx = iota
+    browseNameIdx
+    descriptionIdx
+    accessLevelIdx
+    dataTypeIdx
+    numRequiredAttrs
+)

 func processNodeAttributes(attrs []*ua.DataValue, def *NodeDef, path string, logger Logger) error {
+    // Validate attributes slice length
+    if len(attrs) < numRequiredAttrs {
+        return fmt.Errorf("insufficient attributes: got %d, want %d", len(attrs), numRequiredAttrs)
+    }
+
     // NodeClass
-    if err := handleAttributeStatus("NodeClass", attrs[0], def, path, logger, nodeClassHandler); err != nil {
+    if err := handleAttributeStatus("NodeClass", attrs[nodeClassIdx], def, path, logger, nodeClassHandler); err != nil {

Apply similar changes for other attribute indices throughout the function.

Likely invalid or redundant comment.


20-177: Verify test coverage for critical functions.

The following functions need unit test coverage:

  • handleAttributeStatus
  • handleOKStatus
  • handleNotReadable
✅ Verification successful

Functions are adequately tested through integration tests

The attribute handling functions are thoroughly tested through the Browse functionality tests in opcua_unittest_test.go. The test suite covers:

  • Error handling for nil attributes
  • Invalid attribute scenarios
  • Not readable attributes
  • Security mode issues
  • Various node classes and attribute combinations

The integration tests provide good coverage through mock implementations that simulate real OPC UA node behaviors.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check test coverage for attribute handling functions

echo "=== Searching for existing test cases ==="
rg -A 5 "Test(HandleAttributeStatus|HandleOKStatus|HandleNotReadable)" --type go

echo -e "\n=== Checking test coverage ==="
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out | grep -E "handleAttribute|handleOK|handleNotReadable"

Length of output: 20979


Script:

#!/bin/bash
# Check for test files and any indirect coverage of the target functions

echo "=== Listing test files in opcua_plugin ==="
fd "_test\.go$" opcua_plugin/

echo -e "\n=== Searching for any tests that might indirectly use these functions ==="
rg -l "handleAttribute|processNodeAttributes" opcua_plugin/ --type go

echo -e "\n=== Checking function implementations ==="
rg "^func (handleAttribute|handleOK|handleNotReadable|processNodeAttributes)" opcua_plugin/ --type go

Length of output: 1227


Script:

#!/bin/bash
# Check for indirect testing through browse functionality

echo "=== Checking for browse-related tests ==="
rg -A 10 "func Test.*Browse" opcua_plugin/ --type go

echo -e "\n=== Checking unittest file for relevant tests ==="
cat opcua_plugin/opcua_unittest_test.go

echo -e "\n=== Checking for any test usage of NodeDef struct ==="
rg "NodeDef" opcua_plugin/ --type go -C 5

Length of output: 59310

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants