Skip to content

Commit 1b55ace

Browse files
committed
feat: implement plugin loader for custom rule plugins
Add implementation for loading custom rule plugins from .so files. This provides the core functionality needed to dynamically load and register user-provided rule plugins at runtime. Changes: - Created plugin.go with loadCustomRulePlugin and loadCustomRulePlugins functions - Defined the plugin contract with a fixed function name "AddCustomRules" - Added comprehensive error handling for plugin loading issues - Created basic unit tests for the plugin loading functions The implementation: - Uses Go's native plugin package for loading shared libraries - Follows a simple and well-defined contract for plugin authors - Provides detailed error messages to aid in troubleshooting - Keeps plugin loading separate from the main CLI flow for better modularity This is part 2/3 of the plugin system implementation for issue googleapis#1485. With this change, the api-linter has the core capability to load plugins, but it still needs to be integrated into the CLI flow.
1 parent a67d6ee commit 1b55ace

File tree

2 files changed

+110
-0
lines changed

2 files changed

+110
-0
lines changed

cmd/api-linter/plugin.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"fmt"
19+
"plugin"
20+
21+
"github.com/googleapis/api-linter/lint"
22+
)
23+
24+
// addRulesFuncName is the expected name of the function exported by plugins.
25+
const addRulesFuncName = "AddCustomRules"
26+
27+
// loadCustomRulePlugin loads a plugin from the given path and registers
28+
// its rules with the provided registry.
29+
func loadCustomRulePlugin(pluginPath string, registry lint.RuleRegistry) error {
30+
// Open the plugin
31+
p, err := plugin.Open(pluginPath)
32+
if err != nil {
33+
return fmt.Errorf("failed to open plugin %q: %v", pluginPath, err)
34+
}
35+
36+
// Look up the AddCustomRules function
37+
sym, err := p.Lookup(addRulesFuncName)
38+
if err != nil {
39+
return fmt.Errorf("plugin %q does not export %q function: %v",
40+
pluginPath, addRulesFuncName, err)
41+
}
42+
43+
// Cast to the expected function type
44+
addRulesFunc, ok := sym.(func(lint.RuleRegistry) error)
45+
if !ok {
46+
return fmt.Errorf("plugin %q exports %q with wrong signature",
47+
pluginPath, addRulesFuncName)
48+
}
49+
50+
// Call the function to add custom rules
51+
if err := addRulesFunc(registry); err != nil {
52+
return fmt.Errorf("error registering rules from plugin %q: %v",
53+
pluginPath, err)
54+
}
55+
56+
return nil
57+
}
58+
59+
// loadCustomRulePlugins loads all plugins from the given paths and registers
60+
// their rules with the provided registry.
61+
func loadCustomRulePlugins(pluginPaths []string, registry lint.RuleRegistry) error {
62+
for _, path := range pluginPaths {
63+
if err := loadCustomRulePlugin(path, registry); err != nil {
64+
return err
65+
}
66+
}
67+
return nil
68+
}

cmd/api-linter/plugin_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"testing"
19+
20+
"github.com/googleapis/api-linter/lint"
21+
)
22+
23+
func TestLoadCustomRulePlugins(t *testing.T) {
24+
// Mock registry
25+
mockRegistry := lint.NewRuleRegistry()
26+
27+
// Test with empty paths
28+
if err := loadCustomRulePlugins(nil, mockRegistry); err != nil {
29+
t.Fatalf("loadCustomRulePlugins(nil) = %v; want no error", err)
30+
}
31+
32+
// Test with invalid paths - should return an error
33+
invalidPaths := []string{"nonexistent.so"}
34+
err := loadCustomRulePlugins(invalidPaths, mockRegistry)
35+
if err == nil {
36+
t.Fatalf("loadCustomRulePlugins(%v) = nil; want error", invalidPaths)
37+
}
38+
39+
// Note: Testing with actual plugins would require building real .so files,
40+
// which is complex to set up in unit tests. In a real integration test,
41+
// we would build a test plugin and verify it loads correctly.
42+
}

0 commit comments

Comments
 (0)