-
Notifications
You must be signed in to change notification settings - Fork 152
How to integrate a custom rule into the linter? #1485
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
Comments
Hey @julioz I don't think there is currently an easy way to integrate custom rules with the The api-linter/cmd/api-linter/main.go Lines 27 to 47 in c674736
What would need to happen (as I'm sure you know) is the ApiLinter would need a mechanism for specifying and dynamically loading package-external rules to add them to the One back-of-the-napkin idea would be to use the Go While it's not ideal, writing your own CLI/executor and initializing your own |
Thanks @noahdietz , I'm positively surprised by how responsive you are in this repository, it has certainly not been my experience with other Google-owned open source 😊 Since we start getting into "design-land", I wonder whether you would consider an alternative to Go's plugin library (which is essentially a wrapper on Here's a good example of how the architecture would look like. Of course, this would mean we'd need to apply some changes on how the api-linter is built, to allow for the flexibility, essentially making it an RPC client. Before we spend energy discussing this path, I wonder if it's even something that would be considered in this Google library. |
To document my path for future readers, in order to write a custom lint rule, I have:
With this, I am able to run the custom AIP rule, and also see the $ ../api-linter/api-linter --config api-linter.yaml assessment.proto
- file_path: assessment.proto
problems:
- message: Message names must start with 'Julio'
suggestion: JulioSendAssessmentTasksRequest
location:
start_position:
line_number: 12
column_number: 1
end_position:
line_number: 12
column_number: 37
path: assessment.proto
rule_id: internal::9000::julio-prefix
rule_doc_uri: ""
The ---
- disabled_rules:
- 'internal::9000::julio-prefix'
[1]: package aip9000
import (
"strings"
"github.com/googleapis/api-linter/lint"
"github.com/jhump/protoreflect/desc"
)
var julioPrefix = &lint.MessageRule{
Name: lint.NewRuleName(9000, "julio-prefix"),
OnlyIf: func(m *desc.MessageDescriptor) bool {
return strings.HasSuffix(m.GetName(), "Request") || strings.HasSuffix(m.GetName(), "Response")
},
LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
name := m.GetName()
if !strings.HasPrefix(name, "Julio") {
return []lint.Problem{{
Message: "Message names must start with 'Julio'",
Descriptor: m,
Suggestion: "Julio" + name,
}}
}
return nil
},
} [2]: // Package aip9000 contains custom rules for the organization
package aip9000
import (
"github.com/googleapis/api-linter/lint"
)
// AddRules adds all of the custom rules to the provided registry.
func AddRules(r lint.RuleRegistry) error {
return r.Register(
9000,
julioPrefix,
)
} [3]: package aip9000
import (
"testing"
"github.com/googleapis/api-linter/rules/internal/testutils"
)
func TestJulioPrefix(t *testing.T) {
tests := []struct {
name string
Message string
problems testutils.Problems
}{
{
name: "Valid",
Message: `
message JulioGetBookRequest {
string name = 1;
}
`,
problems: nil,
},
{
name: "Invalid",
Message: `
message GetBookRequest {
string name = 1;
}
`,
problems: testutils.Problems{{
Message: "Message names must start with 'Julio'",
}},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
file := testutils.ParseProto3String(t, test.Message)
message := file.GetMessageTypes()[0]
problems := julioPrefix.Lint(file)
if diff := test.problems.SetDescriptor(message).Diff(problems); diff != "" {
t.Error(diff)
}
})
}
} |
@noahdietz So, looking at the steps identified on the above, we can have a sense of the required integration points, if we were to extend the library to allow for plug-ins. |
You spoke too soon - I've ignored this for a few days now 😆 but seriously, thank you, I appreciate you saying that. I try my best.
Oh, yeah, that's a very useful write up. After reading it I think both approaches have merit, and I'm not 100% sure of either...my core priorities would be as follows:
Those have me still lean more towards shared-library/Go plugin model (we could implement it in a few days with minimal friction for all involved).
Thank you very much for taking the time to include this. The
This is the question isn't it! At the moment, I don't think this would be a priority. My main priority is to begin planning for migration off of jhump/protoreflect, which will require a new major version (😿) . This might one of those things that we look at whilst planning, especially because adding it now would expand the surface that is taking a major version :) Adding it afterwards means we and you have less to worry about/migrate. |
This is great to know. If a major bump is in the near future outlook, this is a great time to start designing what a plugin API would look like. Technically speaking, for the long run I am more inclined to the That said, I am also curious to verify what |
This implementation demonstrates a 'Julio prefix' rule (AIP 9001) as a standalone Go plugin. Key components: 1. Plugin contract: Exports 'AddCustomRules(registry lint.RuleRegistry) error' 2. Plugin structure: - main.go - Entry point exporting AddCustomRules - rules.go - Rule definition - rule_test.go - Unit tests - build.sh - Build script that handles versioning 3. AIP usage: Uses AIP 9001 from the 9000+ block reserved for custom/internal rules as per https://google.aip.dev/adopting This plugin can be loaded dynamically by the api-linter once CLI support is implemented. The plugin must be built with exactly the same Go version and dependency versions as the api-linter binary. References: - Issue discussion: googleapis#1485 - Go plugin docs: https://pkg.go.dev/plugin
Add a new flag to the api-linter CLI that allows specifying custom rule plugins. This is the first step toward supporting a plugin ecosystem for custom rules. Changes: - Added RulePluginPaths field to the cli struct to store plugin file paths - Added --rule-plugin flag that can be used multiple times for multiple plugins - Updated tests to verify the flag works correctly The flag follows the pattern of other path-related flags in the CLI: - Uses StringArrayVar to support multiple values - Consistent naming with other flags (kebab-case) - Clear documentation in the help text This is part of the implementation for issue googleapis#1485, enabling organization-specific custom rules through plugins rather than requiring users to fork the repository. References: - Issue discussion: googleapis#1485 - Go plugin docs: https://pkg.go.dev/plugin
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.
Complete the plugin system implementation by integrating the plugin loader into the CLI workflow. This connects the previously added components to create a fully functional plugin system. Changes: - Updated the lint method to load plugins before running the linter - Added informative message showing how many plugins were loaded - Ensured proper error handling and propagation With this change, the plugin system is now complete and operational. Users can now extend the api-linter with custom rules by: 1. Creating plugins with the AddCustomRules function 2. Building them as shared libraries (.so files) 3. Specifying them with the --rule-plugin flag when running api-linter This completes the 3-part implementation for issue googleapis#1485, providing a way for organizations to extend the linter with custom rules without forking the entire repository.
Hey @noahdietz, I've implemented a working solution for custom rule plugins and submitted it as PR #1487. I went with the Go plugin approach you mentioned in your initial comment. The implementation includes:
As you mentioned, Go's plugin system does have strict version requirements. To address this, I included:
The PR has a detailed description with usage examples and test commands. This approach should be relatively easy to maintain, as it follows the existing pattern of using I understand that with the upcoming major version (moving away from jhump/protoreflect), this might not be the long-term solution. If accepted, it could provide immediate value to users while a more robust RPC-based solution could be considered for the next major version. There are definitely parts we need to iterate there, and I don't expect it to be a direct-merge solution since it's supposed to only be an illustration on how a custom plugin could be implemented. Nevertheless, I'm interested in your review for the overall architecture. Thanks again for your guidance on this. |
While adopting AIPs in my organization, and relying on
api-linter
to perform the validations, we have pretty quickly hit the need of writing our own custom rules, that would extend the behavior of the linter.This page describes how one might adopt AIPs on their orgs, but I found no reference on this repository, nor on the documentation page on what the advised approach is, or the steps to be taken.
Our current workaround is to fork this repository and maintain our own copy of the linter, but that has an embedded operational cost of distributing the binary, keeping it in sync with the upstream remote, etc.
Let's say I'd like to write the simple rule as follows:
The text was updated successfully, but these errors were encountered: