@@ -53,7 +53,7 @@ func analyzeBinaryExpr(pass *analysis.Pass, n ast.Node, generatedFiles filecheck
5353 if ! ok || reported [bin ] {
5454 return
5555 }
56- left , ok := matchSlashSeparator (bin )
56+ left , rightOverride , ok := matchSlashSeparator (bin )
5757 if ! ok {
5858 return
5959 }
@@ -73,6 +73,9 @@ func analyzeBinaryExpr(pass *analysis.Pass, n ast.Node, generatedFiles filecheck
7373
7474 leftText := astutil .NodeText (pass .Fset , left )
7575 rightText := astutil .NodeText (pass .Fset , bin .Y )
76+ if rightOverride != "" {
77+ rightText = rightOverride
78+ }
7679 message := `manual "/" path concatenation; use filepath.Join (or path.Join) instead`
7780 if isShortOperandText (leftText ) && isShortOperandText (rightText ) && ! containsSlashConcat (left ) {
7881 message = fmt .Sprintf (`manual "/" path concatenation; use filepath.Join(%s, %s) (or path.Join) instead` , leftText , rightText )
@@ -84,15 +87,28 @@ func analyzeBinaryExpr(pass *analysis.Pass, n ast.Node, generatedFiles filecheck
8487 })
8588}
8689
87- // analyzeAssignStmt reports compound assignments of the shape X += "/" + Y.
90+ // analyzeAssignStmt reports compound assignments of the shape X += "/" + Y,
91+ // as well as the two-operand X += "/subpath" form.
8892func analyzeAssignStmt (pass * analysis.Pass , n ast.Node , generatedFiles filecheck.GeneratedIndex , noLintIndex nolint.DirectiveIndex ) {
8993 assign , ok := n .(* ast.AssignStmt )
9094 if ! ok || assign .Tok != token .ADD_ASSIGN || len (assign .Lhs ) != 1 || len (assign .Rhs ) != 1 {
9195 return
9296 }
93- bin , ok := assign .Rhs [0 ].(* ast.BinaryExpr )
94- if ! ok || bin .Op != token .ADD || ! isSlashLiteral (bin .X ) {
95- return
97+ var rightExpr ast.Expr
98+ var rightOverride string
99+ switch rhs := assign .Rhs [0 ].(type ) {
100+ case * ast.BinaryExpr :
101+ if rhs .Op != token .ADD || ! isSlashLiteral (rhs .X ) {
102+ return
103+ }
104+ rightExpr = rhs .Y
105+ default :
106+ trimmed , isEmbedded := embeddedSlashLiteral (assign .Rhs [0 ])
107+ if ! isEmbedded {
108+ return
109+ }
110+ rightExpr = assign .Rhs [0 ]
111+ rightOverride = trimmed
96112 }
97113 pos := pass .Fset .PositionFor (assign .Pos (), false )
98114 if filecheck .ShouldSkipFilename (pos .Filename , generatedFiles ) {
@@ -103,7 +119,10 @@ func analyzeAssignStmt(pass *analysis.Pass, n ast.Node, generatedFiles filecheck
103119 }
104120
105121 leftText := astutil .NodeText (pass .Fset , assign .Lhs [0 ])
106- rightText := astutil .NodeText (pass .Fset , bin .Y )
122+ rightText := astutil .NodeText (pass .Fset , rightExpr )
123+ if rightOverride != "" {
124+ rightText = rightOverride
125+ }
107126 message := `manual "/" path concatenation; use filepath.Join (or path.Join) instead`
108127 if isShortOperandText (leftText ) && isShortOperandText (rightText ) && ! containsSlashConcat (assign .Lhs [0 ]) {
109128 message = fmt .Sprintf (`manual "/" path concatenation; use filepath.Join(%s, %s) (or path.Join) instead` , leftText , rightText )
@@ -116,24 +135,28 @@ func analyzeAssignStmt(pass *analysis.Pass, n ast.Node, generatedFiles filecheck
116135}
117136
118137// matchSlashSeparator reports whether bin has the shape X + "/" + Y, which Go
119- // parses as ((X + "/") + Y), and returns the X operand.
120- func matchSlashSeparator (bin * ast.BinaryExpr ) (left ast.Expr , ok bool ) {
138+ // parses as ((X + "/") + Y), or the two-operand shape X + "/subpath", where
139+ // the leading slash is embedded in a longer literal. It returns the X operand
140+ // (left) and, for the two-operand shape, a quoted rightOverride text (the
141+ // literal with its leading slash stripped) suitable for the diagnostic
142+ // message; rightOverride is empty for the three-operand shape, where the
143+ // caller uses bin.Y's own source text instead.
144+ func matchSlashSeparator (bin * ast.BinaryExpr ) (left ast.Expr , rightOverride string , ok bool ) {
121145 if bin .Op != token .ADD {
122- return nil , false
146+ return nil , "" , false
123147 }
124- inner , isBinary := bin .X .(* ast.BinaryExpr )
125- if ! isBinary || inner .Op != token .ADD {
126- return nil , false
127- }
128- if ! isSlashLiteral (inner .Y ) {
129- return nil , false
148+ if inner , isBinary := bin .X .(* ast.BinaryExpr ); isBinary && inner .Op == token .ADD && isSlashLiteral (inner .Y ) {
149+ // A left operand that is itself the separator (e.g. `"/" + "/" + name`)
150+ // carries no path segment to join, so it is not a manual join.
151+ if isSlashLiteral (inner .X ) {
152+ return nil , "" , false
153+ }
154+ return inner .X , "" , true
130155 }
131- // A left operand that is itself the separator (e.g. `"/" + "/" + name`)
132- // carries no path segment to join, so it is not a manual join.
133- if isSlashLiteral (inner .X ) {
134- return nil , false
156+ if trimmed , isEmbedded := embeddedSlashLiteral (bin .Y ); isEmbedded {
157+ return bin .X , trimmed , true
135158 }
136- return inner . X , true
159+ return nil , "" , false
137160}
138161
139162// isSlashLiteral reports whether expr is the string literal "/".
@@ -146,6 +169,23 @@ func isSlashLiteral(expr ast.Expr) bool {
146169 return err == nil && val == "/"
147170}
148171
172+ // embeddedSlashLiteral reports whether expr is a string literal that begins
173+ // with "/" and carries additional path text after it (e.g. "/config.yml"),
174+ // as opposed to the bare separator "/" alone. On success it returns the
175+ // literal's text quoted without the leading slash (e.g. `"config.yml"`),
176+ // suitable for a filepath.Join diagnostic argument.
177+ func embeddedSlashLiteral (expr ast.Expr ) (trimmedQuoted string , ok bool ) {
178+ lit , isLit := expr .(* ast.BasicLit )
179+ if ! isLit || lit .Kind != token .STRING {
180+ return "" , false
181+ }
182+ val , err := strconv .Unquote (lit .Value )
183+ if err != nil || ! strings .HasPrefix (val , "/" ) || val == "/" {
184+ return "" , false
185+ }
186+ return strconv .Quote (strings .TrimPrefix (val , "/" )), true
187+ }
188+
149189// maxOperandTextLen bounds the operand source text embedded in a diagnostic
150190// message so that long or multi-line operands do not produce unreadable output.
151191const maxOperandTextLen = 48
0 commit comments