@@ -3,6 +3,7 @@ package development
33import (
44 "context"
55 "fmt"
6+ "strings"
67
78 "github.com/ccoveille/go-safecast/v2"
89
@@ -64,8 +65,11 @@ func GetWarnings(ctx context.Context, devCtx *DevContext) ([]*devinterface.Devel
6465 res := schema .ResolverForCompiledSchema (* devCtx .CompiledSchema )
6566 ts := schema .NewTypeSystem (res )
6667
68+ // Pre-split schema string once for performance when checking multiple permissions
69+ schemaLines := strings .Split (devCtx .SchemaString , "\n " )
70+
6771 for _ , def := range devCtx .CompiledSchema .ObjectDefinitions {
68- found , err := addDefinitionWarnings (ctx , def , ts )
72+ found , err := addDefinitionWarnings (ctx , def , ts , schemaLines )
6973 if err != nil {
7074 return nil , err
7175 }
@@ -79,7 +83,7 @@ type contextKey string
7983
8084var relationKey = contextKey ("relation" )
8185
82- func addDefinitionWarnings (ctx context.Context , nsDef * corev1.NamespaceDefinition , ts * schema.TypeSystem ) ([]* devinterface.DeveloperWarning , error ) {
86+ func addDefinitionWarnings (ctx context.Context , nsDef * corev1.NamespaceDefinition , ts * schema.TypeSystem , schemaLines [] string ) ([]* devinterface.DeveloperWarning , error ) {
8387 def , err := schema .NewDefinition (ts , nsDef )
8488 if err != nil {
8589 return nil , err
@@ -111,12 +115,125 @@ func addDefinitionWarnings(ctx context.Context, nsDef *corev1.NamespaceDefinitio
111115 }
112116
113117 warnings = append (warnings , found ... )
118+
119+ // Check for mixed operators without parentheses using source scanning
120+ if ! shouldSkipCheck (rel .Metadata , "mixed-operators-without-parentheses" ) {
121+ expressionText := extractPermissionExpression (schemaLines , rel .Name , rel .GetSourcePosition ())
122+ if expressionText != "" {
123+ if warning := CheckExpressionForMixedOperators (rel .Name , expressionText , rel .GetSourcePosition ()); warning != nil {
124+ warnings = append (warnings , warning )
125+ }
126+ }
127+ }
114128 }
115129 }
116130
117131 return warnings , nil
118132}
119133
134+ // extractPermissionExpression extracts the expression text for a permission from the schema source.
135+ // It uses the source position to locate the permission and extracts the text after the '=' sign.
136+ // The schemaLines parameter should be pre-split for performance when checking multiple permissions.
137+ func extractPermissionExpression (schemaLines []string , _ string , sourcePosition * corev1.SourcePosition ) string {
138+ if sourcePosition == nil || len (schemaLines ) == 0 {
139+ return ""
140+ }
141+
142+ lineIdx , err := safecast.Convert [int ](sourcePosition .ZeroIndexedLineNumber )
143+ if err != nil || lineIdx < 0 || lineIdx >= len (schemaLines ) {
144+ return ""
145+ }
146+
147+ // Start from the permission's line and look for the expression
148+ // The expression is everything after '=' until end of statement
149+ var expressionBuilder strings.Builder
150+ foundEquals := false
151+ parenDepth := 0
152+ inBlockComment := false
153+ lastNonWhitespaceWasOperator := false
154+
155+ for i := lineIdx ; i < len (schemaLines ); i ++ {
156+ line := schemaLines [i ]
157+
158+ // If this is the first line, start from the column position
159+ startCol := 0
160+ if i == lineIdx {
161+ colPos , err := safecast.Convert [int ](sourcePosition .ZeroIndexedColumnPosition )
162+ if err == nil && colPos < len (line ) {
163+ startCol = colPos
164+ }
165+ }
166+
167+ lineHasContent := false
168+ for j := startCol ; j < len (line ); j ++ {
169+ ch := line [j ]
170+
171+ // Handle block comment state
172+ if inBlockComment {
173+ if ch == '*' && j + 1 < len (line ) && line [j + 1 ] == '/' {
174+ inBlockComment = false
175+ j ++ // Skip the '/'
176+ }
177+ continue
178+ }
179+
180+ // Check for start of block comment
181+ if ch == '/' && j + 1 < len (line ) && line [j + 1 ] == '*' {
182+ inBlockComment = true
183+ j ++ // Skip the '*'
184+ continue
185+ }
186+
187+ // Check for line comment - skip rest of line
188+ if ch == '/' && j + 1 < len (line ) && line [j + 1 ] == '/' {
189+ break
190+ }
191+
192+ if ! foundEquals {
193+ if ch == '=' {
194+ foundEquals = true
195+ }
196+ continue
197+ }
198+
199+ // Track parenthesis depth for multi-line expressions
200+ switch ch {
201+ case '(' :
202+ parenDepth ++
203+ case ')' :
204+ parenDepth --
205+ }
206+
207+ // Track if this is an operator (for multi-line continuation detection)
208+ isWhitespaceChar := ch == ' ' || ch == '\t'
209+ if ! isWhitespaceChar {
210+ lineHasContent = true
211+ lastNonWhitespaceWasOperator = (ch == '+' || ch == '-' || ch == '&' )
212+ }
213+
214+ expressionBuilder .WriteByte (ch )
215+ }
216+
217+ // Determine if expression continues on next line:
218+ // 1. We're inside parentheses (parenDepth > 0)
219+ // 2. We're inside a block comment
220+ // 3. The line ended with an operator (suggesting continuation)
221+ // 4. The line had no content yet after '=' (e.g., '= \n foo + bar')
222+ if foundEquals {
223+ continueToNextLine := parenDepth > 0 || inBlockComment || lastNonWhitespaceWasOperator || ! lineHasContent
224+ if ! continueToNextLine {
225+ break
226+ }
227+ // Add space for multi-line expressions
228+ if i < len (schemaLines )- 1 {
229+ expressionBuilder .WriteByte (' ' )
230+ }
231+ }
232+ }
233+
234+ return strings .TrimSpace (expressionBuilder .String ())
235+ }
236+
120237func shouldSkipCheck (metadata * corev1.Metadata , name string ) bool {
121238 if metadata == nil {
122239 return false
0 commit comments