diff --git a/.github/prompts/migration.prompt.md b/.github/prompts/migration.prompt.md new file mode 100644 index 000000000..206164252 --- /dev/null +++ b/.github/prompts/migration.prompt.md @@ -0,0 +1,151 @@ +# Task + +Convert the selected Java source into idiomatic Go **with minimal semantic drift**. + +- Map packages to directories and package names. +- Map classes (including abstract) and their fields/methods to Go **interface** and **unexported implementation structs**. +- Preserve logic while adapting to Go naming, encapsulation and error handling. +- Keep diffs focused: one Java file -> one Go file (small support code if strictly necessary). + +## Inputs + +- Java file(s) to convert. +- Any related types the file depends on. +- Package path target within the Go module. + +## Outputs + +- A new Go file with idiomatic code. + +## Package & File Mapping + +1. **One-to-one element mapping** + - **Files**: Map each Java source file `.java` to to one go file, named `lowercase-with-dashes.go` (e.g., `FooBar.java` -> `foo-bar.go`). Be consistent within the package. + - **Classes/Interfaces**: For each Java class or interface define a **Go interface** named after the Java type (exported) and an **unexported implementation struct**. + - **Methods**: Map each java method to a Go method or function. For overloads, pick distinct names (e.g., `Advance`, `AdvanceN`). + +2. **Package structure** + - Java: `package com.example.foo.something;` + - Go: directory `foo/something` with `package something` in the file. + +## Types & Encapsulation + +### Classes -> Interface + Impl Struct + +- Define an **interface** named exactly after the Java class, e.g. `Foo`. +- Define a constructor `NewFoo(...) Foo` returning the interface. +- Define an **unexported** struct `fooImpl` implements the interface. Prefer composition/embedding for reuse. + +### Fields -> Struct Fields + +- Private/encapsulated state lives in unexported struct fields (e.g., `bar int`). +- Provide getters/setters as interface methods only if the Java API requires them to be public. Avoid exporting fields directly unless they are immutable configuration. + +### Constructors + +For each Java public constructor: + +```go +func NewFoo(params) Foo { + return &fooImpl{/* initialize fields */} +} +``` + +- If multiple Java constructors exist, use either distinct names (`NewFooWithParams`) **or** the functional options pattern for optional params (avoid overloading). + +### Methods & Receivers + +- **Non-mutating** -> value receiver if the struct is small and the method is read-only. +- **Mutating** -> pointer receiver. +- Prefer returning `(T, error)` over panicking; translate Java exceptions to `error` values when they cross API boundaries. + +### Abstract classes + +```go +type Foo interface { + // abstract methods + requires accessors +} + +type fooBase struct {/* shared fields */} + +func (b fooBase) GetX() T { return b.x } + +type fooImpl struct { + fooBase +} +func NewFoo(params) Foo { + return &fooImpl{ + fooBase: fooBase{/* initialize shared fields */}, + } +} +``` + +### Method overloading + +Java: + +```java +void advance(); +void advance(int n); +``` + +Go: + +```go +func (r *readerImpl) Advance() {} +func (r *readerImpl) AdvanceN(n int) {} +``` + +### Equality / Hashing + +- If Java only overrides `equals()`/`hashCode()`, in Go prefer: + - Use direct `==` for comparable structs; or + - Provide an explicit **lookup key**: + + ```go + type FooLookupKey struct { Bar int; Baz string } + func (f *fooImpl) FooLookupKey() FooLookupKey { return FooLookupKey{f.bar, f.baz} } + ``` + +## Naming & Comments + +- **Packages:** short, lowercase, single word (`something`). +- **Exports:** capitalize to export. Keep names concise and avoid stutter (prefer `something.Reader` with type name `Reader`). + +## Error Handling + +- Always return `(T, error)` for fallible operations. Don't use `panic` for normal control flow. +- Wrap lower-level errors with context using `fmt.Errorf("op: %w", err)` so callers can use `errors.Is/As`. +- Match errors with `errors.Is` (sentinels) or `errors.As` (typed errors with additional context). + +### Java exceptions -> Go typed errors + +- Define Java-specific exceptions as typed errros in `common/errors/errors.go` (package `errors`). + +```go +package errors + +import "fmt" + +type IndexOutOfBoundsError struct { + index int + length int +} + +func (e IndexOutOfBoundsError) Error() string { + return fmt.Sprintf("Index %d out of bounds for length %d", e.index, e.length) +} + +func (e IndexOutOfBoundsError) GetIndex() int { return e.index } +func (e IndexOutOfBoundsError) GetLength() int { return e.length } +``` + +## Generics + +- Map Java generics to Go generics when needed. + +## Guardrails & Non goals + +- **Do no** add file headers or license comments. +- **Do not** introduce new public APIs unless requires by the Java surface. +- **Do not** add comments unless the Java source has them. diff --git a/common/errors/errors.go b/common/errors/errors.go new file mode 100644 index 000000000..2884ccfd7 --- /dev/null +++ b/common/errors/errors.go @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package errors + +import "fmt" + +type IndexOutOfBoundsError struct { + index int + length int +} + +func (e IndexOutOfBoundsError) Error() string { + return fmt.Sprintf("Index %d out of bounds for length %d", e.index, e.length) +} + +func (e IndexOutOfBoundsError) GetIndex() int { + return e.index +} + +func (e IndexOutOfBoundsError) GetLength() int { + return e.length +} + +func NewIndexOutOfBoundsError(index, length int) *IndexOutOfBoundsError { + return &IndexOutOfBoundsError{ + index: index, + length: length, + } +} + +type IllegalArgumentError struct { + argument any +} + +func (e IllegalArgumentError) Error() string { + return fmt.Sprintf("Illegal argument: %v", e.argument) +} + +func (e IllegalArgumentError) GetArgument() any { + return e.argument +} + +func NewIllegalArgumentError(argument any) *IllegalArgumentError { + return &IllegalArgumentError{ + argument: argument, + } +} diff --git a/tools/diagnostics/default-diagnostic.go b/tools/diagnostics/default-diagnostic.go new file mode 100644 index 000000000..2c133e98f --- /dev/null +++ b/tools/diagnostics/default-diagnostic.go @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package diagnostics + +import ( + "fmt" + + "ballerina-lang-go/tools/text" +) + +// DefaultDiagnostic is an internal implementation of the Diagnostic interface that is used by the DiagnosticFactory +// to create diagnostics. +type DefaultDiagnostic interface { + Diagnostic +} + +type defaultDiagnosticImpl struct { + diagnosticBase + diagnosticInfo DiagnosticInfo + location Location + properties []DiagnosticProperty[any] + message string +} + +func NewDefaultDiagnostic(diagnosticInfo DiagnosticInfo, location Location, properties []DiagnosticProperty[any], args ...any) DefaultDiagnostic { + message := formatMessage(diagnosticInfo.MessageFormat(), args...) + return &defaultDiagnosticImpl{ + diagnosticInfo: diagnosticInfo, + location: location, + properties: properties, + message: message, + } +} + +func (dd *defaultDiagnosticImpl) Location() Location { + return dd.location +} + +func (dd *defaultDiagnosticImpl) DiagnosticInfo() DiagnosticInfo { + return dd.diagnosticInfo +} + +func (dd *defaultDiagnosticImpl) Message() string { + return dd.message +} + +func (dd *defaultDiagnosticImpl) Properties() []DiagnosticProperty[any] { + return dd.properties +} + +func (dd *defaultDiagnosticImpl) String() string { + lineRange := dd.location.LineRange() + filePath := lineRange.FileName() + + startLine := lineRange.StartLine() + endLine := lineRange.EndLine() + + oneBasedStartLine := text.LinePositionFromLineAndOffset(startLine.Line()+1, startLine.Offset()+1) + oneBasedEndLine := text.LinePositionFromLineAndOffset(endLine.Line()+1, endLine.Offset()+1) + oneBasedLineRange := text.LineRangeFromLinePositions(filePath, oneBasedStartLine, oneBasedEndLine) + + return fmt.Sprintf("%s [%s:%s] %s", + dd.diagnosticInfo.Severity().String(), + filePath, + oneBasedLineRange.String(), + dd.Message()) +} + +func formatMessage(format string, args ...any) string { + if len(args) == 0 { + return format + } + return fmt.Sprintf(format, args...) +} diff --git a/tools/diagnostics/diagnostic-code.go b/tools/diagnostics/diagnostic-code.go new file mode 100644 index 000000000..df621d3de --- /dev/null +++ b/tools/diagnostics/diagnostic-code.go @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package diagnostics + +// DiagnosticCode represents a diagnostic code. +// Diagnostic code uniquely identifies a diagnostic. +type DiagnosticCode interface { + Severity() DiagnosticSeverity + DiagnosticId() string + MessageKey() string +} diff --git a/tools/diagnostics/diagnostic-factory.go b/tools/diagnostics/diagnostic-factory.go new file mode 100644 index 000000000..2f5cad000 --- /dev/null +++ b/tools/diagnostics/diagnostic-factory.go @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package diagnostics + +// CreateDiagnostic creates a Diagnostic instance from the given details. +// +// Parameters: +// - diagnosticInfo: static diagnostic information +// - location: the location of the diagnostic +// - args: arguments to diagnostic message format +// +// Returns a Diagnostic instance. +func CreateDiagnostic(diagnosticInfo DiagnosticInfo, location Location, args ...any) Diagnostic { + return NewDefaultDiagnostic(diagnosticInfo, location, nil, args...) +} + +// CreateDiagnosticWithProperties creates a Diagnostic instance from the given details. +// +// Parameters: +// - diagnosticInfo: static diagnostic information +// - location: the location of the diagnostic +// - properties: properties associated with the diagnostic +// - args: arguments to diagnostic message format +// +// Returns a Diagnostic instance. +func CreateDiagnosticWithProperties(diagnosticInfo DiagnosticInfo, location Location, properties []DiagnosticProperty[any], args ...any) Diagnostic { + return NewDefaultDiagnostic(diagnosticInfo, location, properties, args...) +} diff --git a/tools/diagnostics/diagnostic-info.go b/tools/diagnostics/diagnostic-info.go new file mode 100644 index 000000000..4afc89452 --- /dev/null +++ b/tools/diagnostics/diagnostic-info.go @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package diagnostics + +// DiagnosticInfo represents an abstract shape of a Diagnostic that is independent of +// the location and message arguments. +type DiagnosticInfo interface { + Code() string + MessageFormat() string + Severity() DiagnosticSeverity + DiagnosticInfoLookupKey() DiagnosticInfoLookupKey +} + +// DiagnosticInfoLookupKey represents the comparable fields of DiagnosticInfo for equality/hashing. +type DiagnosticInfoLookupKey struct { + Code *string // pointer to handle nil values + MessageFormat string + Severity DiagnosticSeverity +} + +type diagnosticInfoImpl struct { + code *string // pointer to handle nil values + messageFormat string + severity DiagnosticSeverity +} + +// NewDiagnosticInfo constructs an abstract shape of a Diagnostic. +// +// Parameters: +// - code: a code that can be used to uniquely identify a diagnostic category +// - messageFormat: a pattern that can be formatted with message formatting utilities +// - severity: the severity of the diagnostic +func NewDiagnosticInfo(code *string, messageFormat string, severity DiagnosticSeverity) DiagnosticInfo { + return &diagnosticInfoImpl{ + code: code, + messageFormat: messageFormat, + severity: severity, + } +} + +func (di diagnosticInfoImpl) Code() string { + if di.code == nil { + return "" + } + return *di.code +} + +func (di diagnosticInfoImpl) MessageFormat() string { + return di.messageFormat +} + +func (di diagnosticInfoImpl) Severity() DiagnosticSeverity { + return di.severity +} + +// DiagnosticInfoLookupKey returns the lookup key for equality comparisons. +func (di diagnosticInfoImpl) DiagnosticInfoLookupKey() DiagnosticInfoLookupKey { + return DiagnosticInfoLookupKey{ + Code: di.code, + MessageFormat: di.messageFormat, + Severity: di.severity, + } +} diff --git a/tools/diagnostics/diagnostic-property-kind.go b/tools/diagnostics/diagnostic-property-kind.go new file mode 100644 index 000000000..e66a7a6e8 --- /dev/null +++ b/tools/diagnostics/diagnostic-property-kind.go @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package diagnostics + +// DiagnosticPropertyKind represents the kind of the diagnostic property. +type DiagnosticPropertyKind uint8 + +const ( + Symbolic DiagnosticPropertyKind = iota + String + Numeric + Collection + Other +) + +// String returns the string representation of the diagnostic property kind. +func (dpk DiagnosticPropertyKind) String() string { + switch dpk { + case Symbolic: + return "SYMBOLIC" + case String: + return "STRING" + case Numeric: + return "NUMERIC" + case Collection: + return "COLLECTION" + case Other: + return "OTHER" + default: + return "UNKNOWN" + } +} diff --git a/tools/diagnostics/diagnostic-property.go b/tools/diagnostics/diagnostic-property.go new file mode 100644 index 000000000..909bf32e7 --- /dev/null +++ b/tools/diagnostics/diagnostic-property.go @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package diagnostics + +// DiagnosticProperty represents properties passed when diagnostic logging. +type DiagnosticProperty[T any] interface { + Kind() DiagnosticPropertyKind + Value() T +} diff --git a/tools/diagnostics/diagnostic-related-information.go b/tools/diagnostics/diagnostic-related-information.go new file mode 100644 index 000000000..89c6bc046 --- /dev/null +++ b/tools/diagnostics/diagnostic-related-information.go @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package diagnostics + +// DiagnosticRelatedInformation represents a message and location related to a particular Diagnostic. +// A sample usage would be to record all symbol information related to duplicate symbol error. +type DiagnosticRelatedInformation interface { + Location() Location + Message() string +} + +type diagnosticRelatedInformationImpl struct { + location Location + message string +} + +func NewDiagnosticRelatedInformation(location Location, message string) DiagnosticRelatedInformation { + return &diagnosticRelatedInformationImpl{ + location: location, + message: message, + } +} + +func (dri diagnosticRelatedInformationImpl) Location() Location { + return dri.location +} + +func (dri diagnosticRelatedInformationImpl) Message() string { + return dri.message +} diff --git a/tools/diagnostics/diagnostic-severity.go b/tools/diagnostics/diagnostic-severity.go new file mode 100644 index 000000000..047047a8f --- /dev/null +++ b/tools/diagnostics/diagnostic-severity.go @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package diagnostics + +// DiagnosticSeverity represents a severity of a Diagnostic. +type DiagnosticSeverity uint8 + +const ( + Internal DiagnosticSeverity = iota + Hint + Info + Warning + Error +) + +func (ds DiagnosticSeverity) String() string { + switch ds { + case Internal: + return "INTERNAL" + case Hint: + return "HINT" + case Info: + return "INFO" + case Warning: + return "WARNING" + case Error: + return "ERROR" + default: + return "UNKNOWN" + } +} diff --git a/tools/diagnostics/diagnostic.go b/tools/diagnostics/diagnostic.go new file mode 100644 index 000000000..f9cfb18cc --- /dev/null +++ b/tools/diagnostics/diagnostic.go @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package diagnostics + +import "fmt" + +// Diagnostic represents a diagnostic message (error, warning, etc.) with location information. +// A diagnostic represents a compiler error, a warning or a message at a specific location in the source file. +type Diagnostic interface { + Location() Location + DiagnosticInfo() DiagnosticInfo + Message() string + Properties() []DiagnosticProperty[any] + String() string +} + +type diagnosticBase struct{} + +// String returns a string representation of the diagnostic. +// This is the default implementation from the abstract Diagnostic class. +func (db diagnosticBase) String(d Diagnostic) string { + var location string + if d.Location().LineRange().FileName() == "" { + location = "" + } else { + location = fmt.Sprintf(" [%s:%s]", + d.Location().LineRange().FileName(), + d.Location().LineRange().String()) + } + return fmt.Sprintf("%s%s %s", + d.DiagnosticInfo().Severity().String(), + location, + d.Message()) +} diff --git a/tools/diagnostics/location.go b/tools/diagnostics/location.go new file mode 100644 index 000000000..075c3a0ea --- /dev/null +++ b/tools/diagnostics/location.go @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package diagnostics + +import "ballerina-lang-go/tools/text" + +// Location represents the location in TextDocument. +// It is a combination of source file path, start and end line numbers, and start and end column numbers. +type Location interface { + LineRange() text.LineRange + TextRange() text.TextRange +} diff --git a/tools/text/char-reader.go b/tools/text/char-reader.go new file mode 100644 index 000000000..52b8a18eb --- /dev/null +++ b/tools/text/char-reader.go @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +import ( + "unicode" + "unicode/utf8" +) + +// CharReader is a character reader utility used by the Ballerina lexer. +type CharReader interface { + Reset(offset int) + Peek() rune + PeekN(k int) rune + Advance() + AdvanceN(k int) + Mark() + GetMarkedChars() string + IsEOF() bool +} + +// charReaderImpl is the concrete implementation of CharReader. +type charReaderImpl struct { + charBuffer string + offset int + charBufferLength int + lexemeStartPos int +} + +func CharReaderFromTextDocument(textDocument TextDocument) CharReader { + return CharReaderFromText(textDocument.String()) +} + +func CharReaderFromText(text string) CharReader { + return &charReaderImpl{ + charBuffer: text, + offset: 0, + charBufferLength: len(text), + lexemeStartPos: 0, + } +} + +func (cr *charReaderImpl) Reset(offset int) { + cr.offset = offset +} + +func (cr charReaderImpl) Peek() rune { + if cr.offset < cr.charBufferLength { + r, _ := utf8.DecodeRuneInString(cr.charBuffer[cr.offset:]) + return r + } else { + // TODO Revisit this branch + return unicode.MaxRune + } +} + +func (cr charReaderImpl) PeekN(k int) rune { + n := cr.offset + for range k { + _, size := utf8.DecodeRuneInString(cr.charBuffer[n:]) + n = n + size + } + + if n < cr.charBufferLength { + r, _ := utf8.DecodeRuneInString(cr.charBuffer[n:]) + return r + } else { + // TODO Revisit this branch + return unicode.MaxRune + } +} + +func (cr *charReaderImpl) Advance() { + _, size := utf8.DecodeRuneInString(cr.charBuffer[cr.offset:]) + cr.offset = cr.offset + size +} + +func (cr *charReaderImpl) AdvanceN(k int) { + for range k { + if cr.offset < cr.charBufferLength { + _, size := utf8.DecodeRuneInString(cr.charBuffer[cr.offset:]) + cr.offset = cr.offset + size + } + } +} + +func (cr *charReaderImpl) Mark() { + cr.lexemeStartPos = cr.offset +} + +func (cr charReaderImpl) GetMarkedChars() string { + return cr.charBuffer[cr.lexemeStartPos:cr.offset] +} + +func (cr charReaderImpl) IsEOF() bool { + return cr.offset >= cr.charBufferLength +} diff --git a/tools/text/line-map.go b/tools/text/line-map.go new file mode 100644 index 000000000..bd1044e59 --- /dev/null +++ b/tools/text/line-map.go @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +import ( + "fmt" + + "ballerina-lang-go/common/errors" +) + +// LineMap represents a collection of text lines in the TextDocument. +type LineMap interface { + TextLine(line int) (TextLine, error) + LinePositionFromPosition(position int) (LinePosition, error) + TextPositionFromLinePosition(linePosition LinePosition) (int, error) + TextLines() []string +} + +type lineMapImpl struct { + textLines []TextLine + length int +} + +func NewLineMap(textLines []TextLine) LineMap { + return &lineMapImpl{ + textLines: textLines, + length: len(textLines), + } +} + +func (lm lineMapImpl) TextLine(line int) (TextLine, error) { + if err := lm.lineRangeCheck(line); err != nil { + return nil, err + } + return lm.textLines[line], nil +} + +func (lm lineMapImpl) LinePositionFromPosition(position int) (LinePosition, error) { + if err := lm.positionRangeCheck(position); err != nil { + return nil, err + } + textLine := lm.findLineFrom(position) + return LinePositionFromLineAndOffset(textLine.LineNo(), position-textLine.StartOffset()), nil +} + +func (lm lineMapImpl) TextPositionFromLinePosition(linePosition LinePosition) (int, error) { + if err := lm.lineRangeCheck(linePosition.Line()); err != nil { + return -1, err + } + textLine := lm.textLines[linePosition.Line()] + if textLine.Length() < linePosition.Offset() { + return -1, errors.NewIllegalArgumentError(fmt.Sprintf("Cannot find a line with the character offset '%d'", linePosition.Offset())) + } + // TODO: Lazy initialize and cache + return textLine.StartOffset() + linePosition.Offset(), nil +} + +func (lm lineMapImpl) TextLines() []string { + lines := make([]string, len(lm.textLines)) + for i, textLine := range lm.textLines { + lines[i] = textLine.Text() + } + return lines +} + +func (lm lineMapImpl) positionRangeCheck(position int) error { + if position < 0 || position > lm.textLines[lm.length-1].EndOffset() { + return errors.NewIndexOutOfBoundsError(position, lm.textLines[lm.length-1].EndOffset()) + } + return nil +} + +func (lm lineMapImpl) lineRangeCheck(lineNo int) error { + if lineNo < 0 || lineNo > lm.length { + return errors.NewIndexOutOfBoundsError(lineNo, lm.length) + } + return nil +} + +// findLineFrom returns the TextLine to which the given position belongs. +// Performs a binary search to find the matching text line. +func (lm lineMapImpl) findLineFrom(position int) TextLine { + // Check boundary conditions + if position == 0 { + return lm.textLines[0] + } else if position == lm.textLines[lm.length-1].EndOffset() { + return lm.textLines[lm.length-1] + } + left := 0 + right := lm.length - 1 + for left <= right { + lhs := left >> 1 + rhs := right >> 1 + middle := (lhs + rhs) + (left & right & 1) + startOffset := lm.textLines[middle].StartOffset() + endOffset := lm.textLines[middle].EndOffsetWithNewLines() + if startOffset <= position && position < endOffset { + return lm.textLines[middle] + } else if endOffset <= position { + left = middle + 1 + } else { + right = middle - 1 + } + } + // This should never happen given the boundary checks above + panic("binary search failed to find matching text line") +} diff --git a/tools/text/line-position.go b/tools/text/line-position.go new file mode 100644 index 000000000..aeafaaf06 --- /dev/null +++ b/tools/text/line-position.go @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +import "fmt" + +// LinePosition represents a line number and a character offset from the start of the line. +type LinePosition interface { + Line() int + Offset() int + String() string + LinePositionLookupKey() LinePositionLookupKey +} + +// LinePositionLookupKey represents the comparable fields of LinePosition for equality/hashing. +type LinePositionLookupKey struct { + Line int + Offset int +} + +type linePositionImpl struct { + line int + offset int +} + +func LinePositionFromLineAndOffset(line, offset int) LinePosition { + return &linePositionImpl{ + line: line, + offset: offset, + } +} + +func (lp linePositionImpl) Line() int { + return lp.line +} + +func (lp linePositionImpl) Offset() int { + return lp.offset +} + +func (lp linePositionImpl) String() string { + return fmt.Sprintf("%d:%d", lp.line, lp.offset) +} + +// LinePositionLookupKey returns the lookup key for equality comparisons. +func (lp linePositionImpl) LinePositionLookupKey() LinePositionLookupKey { + return LinePositionLookupKey{ + Line: lp.line, + Offset: lp.offset, + } +} diff --git a/tools/text/line-range.go b/tools/text/line-range.go new file mode 100644 index 000000000..6aac6c41e --- /dev/null +++ b/tools/text/line-range.go @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +import "fmt" + +// LineRange represents a pair of LinePosition. +type LineRange interface { + FileName() string + StartLine() LinePosition + EndLine() LinePosition + String() string + LineRangeLookupKey() LineRangeLookupKey +} + +// LineRangeLookupKey represents the comparable fields of LineRange for equality/hashing. +type LineRangeLookupKey struct { + StartLine LinePositionLookupKey + EndLine LinePositionLookupKey +} + +type lineRangeImpl struct { + fileName string + startLine LinePosition + endLine LinePosition +} + +func LineRangeFromLinePositions(fileName string, startLine, endLine LinePosition) LineRange { + return &lineRangeImpl{ + fileName: fileName, + startLine: startLine, + endLine: endLine, + } +} + +// FileName returns the file name. +func (lr lineRangeImpl) FileName() string { + return lr.fileName +} + +func (lr lineRangeImpl) StartLine() LinePosition { + return lr.startLine +} + +func (lr lineRangeImpl) EndLine() LinePosition { + return lr.endLine +} + +func (lr lineRangeImpl) String() string { + return fmt.Sprintf("(%s,%s)", lr.startLine.String(), lr.endLine.String()) +} + +// LineRangeLookupKey returns the lookup key for equality comparisons. +func (lr lineRangeImpl) LineRangeLookupKey() LineRangeLookupKey { + return LineRangeLookupKey{ + StartLine: lr.startLine.LinePositionLookupKey(), + EndLine: lr.endLine.LinePositionLookupKey(), + } +} diff --git a/tools/text/string-text-document.go b/tools/text/string-text-document.go new file mode 100644 index 000000000..b8458920d --- /dev/null +++ b/tools/text/string-text-document.go @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +import "strings" + +const ( + CR = 13 // Carriage Return + LF = 10 // Line Feed +) + +// StringTextDocument represents a TextDocument created with a string. +type StringTextDocument interface { + TextDocument + String() string +} + +type stringTextDocumentImpl struct { + textDocumentBase + text string + textLineMap LineMap +} + +func NewStringTextDocument(text string) StringTextDocument { + return &stringTextDocumentImpl{ + text: text, + } +} + +func (std *stringTextDocumentImpl) Apply(textDocumentChange TextDocumentChange) TextDocument { + startOffset := 0 + var sb strings.Builder + textEditCount := textDocumentChange.GetTextEditCount() + for i := range textEditCount { + textEdit := textDocumentChange.GetTextEdit(i) + textRange := textEdit.Range() + sb.WriteString(std.text[startOffset:textRange.StartOffset()]) + sb.WriteString(textEdit.Text()) + startOffset = textRange.EndOffset() + } + sb.WriteString(std.text[startOffset:]) + return NewStringTextDocument(sb.String()) +} + +func (std *stringTextDocumentImpl) PopulateTextLineMap() LineMap { + if std.textLineMap != nil { + return std.textLineMap + } + std.textLineMap = NewLineMap(std.calculateTextLines()) + return std.textLineMap +} + +func (std *stringTextDocumentImpl) ToCharArray() []rune { + return []rune(std.text) +} + +func (std *stringTextDocumentImpl) String() string { + return std.text +} + +func (std stringTextDocumentImpl) TextLines() []string { + return std.Lines().TextLines() +} + +func (std *stringTextDocumentImpl) Lines() LineMap { + if std.lineMap != nil { + return std.lineMap + } + std.lineMap = std.PopulateTextLineMap() + return std.lineMap +} + +func (std *stringTextDocumentImpl) calculateTextLines() []TextLine { + var textLines []TextLine + + line := 0 + startOffset := 0 + + index := 0 + textLength := len(std.text) + + var lengthOfNewLineChars int + + for index < textLength { + if std.text[index] == CR || std.text[index] == LF { + nextIndex := index + 1 + if std.text[index] == CR && nextIndex < textLength && std.text[nextIndex] == LF { + lengthOfNewLineChars = 2 + } else { + lengthOfNewLineChars = 1 + } + + endOffset := startOffset + (index - startOffset) + textLines = append(textLines, NewTextLine(line, std.text[startOffset:index], startOffset, endOffset, lengthOfNewLineChars)) + + line = line + 1 + startOffset = endOffset + lengthOfNewLineChars + index = index + lengthOfNewLineChars + } else { + index = index + 1 + } + } + + textLines = append(textLines, NewTextLine(line, std.text[startOffset:], startOffset, textLength, 0)) + + return textLines +} diff --git a/tools/text/text-document-change.go b/tools/text/text-document-change.go new file mode 100644 index 000000000..5ec38240d --- /dev/null +++ b/tools/text/text-document-change.go @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +import "strings" + +// TextDocumentChange represents textual changes on a single TextDocument. +type TextDocumentChange interface { + GetTextEditCount() int + GetTextEdit(index int) TextEdit + String() string +} + +type textDocumentChangeImpl struct { + textEdits []TextEdit +} + +func TextDocumentChangeFromTextEdits(textEdits []TextEdit) TextDocumentChange { + // Create a copy of the slice to ensure immutability + editsCopy := make([]TextEdit, len(textEdits)) + copy(editsCopy, textEdits) + + return &textDocumentChangeImpl{ + textEdits: editsCopy, + } +} + +func (tdc textDocumentChangeImpl) GetTextEditCount() int { + return len(tdc.textEdits) +} + +func (tdc textDocumentChangeImpl) GetTextEdit(index int) TextEdit { + return tdc.textEdits[index] +} + +func (tdc textDocumentChangeImpl) String() string { + var editStrings []string + for _, textEdit := range tdc.textEdits { + editStrings = append(editStrings, textEdit.String()) + } + + return strings.Join(editStrings, ",") +} diff --git a/tools/text/text-document.go b/tools/text/text-document.go new file mode 100644 index 000000000..863edb01a --- /dev/null +++ b/tools/text/text-document.go @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +// TextDocument is an abstract representation of a Ballerina source file (.bal). +type TextDocument interface { + Apply(textDocumentChange TextDocumentChange) TextDocument + ToCharArray() []rune + Line(line int) (TextLine, error) + LinePositionFromTextPosition(textPosition int) (LinePosition, error) + TextPositionFromLinePosition(linePosition LinePosition) (int, error) + TextLines() []string + Lines() LineMap + PopulateTextLineMap() LineMap + String() string +} + +type textDocumentBase struct { + lineMap LineMap +} + +func (td textDocumentBase) Line(line int) (TextLine, error) { + return td.lineMap.TextLine(line) +} + +func (td textDocumentBase) LinePositionFromTextPosition(textPosition int) (LinePosition, error) { + return td.lineMap.LinePositionFromPosition(textPosition) +} + +func (td textDocumentBase) TextPositionFromLinePosition(linePosition LinePosition) (int, error) { + return td.lineMap.TextPositionFromLinePosition(linePosition) +} diff --git a/tools/text/text-documents.go b/tools/text/text-documents.go new file mode 100644 index 000000000..7069f6120 --- /dev/null +++ b/tools/text/text-documents.go @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +// TextDocumentFromText creates a TextDocument from the given text string. +func TextDocumentFromText(text string) TextDocument { + return NewStringTextDocument(text) +} diff --git a/tools/text/text-edit.go b/tools/text/text-edit.go new file mode 100644 index 000000000..966218944 --- /dev/null +++ b/tools/text/text-edit.go @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +import "fmt" + +// TextEdit represents a text edit on a TextDocument. +type TextEdit interface { + Range() TextRange + Text() string + String() string +} + +type textEditImpl struct { + textRange TextRange + text string +} + +func TextEditFromTextRangeAndText(textRange TextRange, text string) TextEdit { + return &textEditImpl{ + textRange: textRange, + text: text, + } +} + +func (te textEditImpl) Range() TextRange { + return te.textRange +} + +func (te textEditImpl) Text() string { + return te.text +} + +func (te textEditImpl) String() string { + return fmt.Sprintf("%s%s", te.textRange.String(), te.text) +} diff --git a/tools/text/text-line.go b/tools/text/text-line.go new file mode 100644 index 000000000..733838b77 --- /dev/null +++ b/tools/text/text-line.go @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +// TextLine represents a single line in the TextDocument. +type TextLine interface { + LineNo() int + Text() string + StartOffset() int + EndOffset() int + EndOffsetWithNewLines() int + Length() int + LengthWithNewLineChars() int +} + +type textLineImpl struct { + lineNo int + text string + startOffset int + endOffset int + lengthOfNewLineChars int +} + +func NewTextLine(lineNo int, text string, startOffset, endOffset, lengthOfNewLineChars int) TextLine { + return &textLineImpl{ + lineNo: lineNo, + text: text, + startOffset: startOffset, + endOffset: endOffset, + lengthOfNewLineChars: lengthOfNewLineChars, + } +} + +func (tl textLineImpl) LineNo() int { + return tl.lineNo +} + +func (tl textLineImpl) Text() string { + return tl.text +} + +func (tl textLineImpl) StartOffset() int { + return tl.startOffset +} + +func (tl textLineImpl) EndOffset() int { + return tl.endOffset +} + +func (tl textLineImpl) EndOffsetWithNewLines() int { + return tl.endOffset + tl.lengthOfNewLineChars +} + +func (tl textLineImpl) Length() int { + return tl.endOffset - tl.startOffset +} + +func (tl textLineImpl) LengthWithNewLineChars() int { + return tl.endOffset - tl.startOffset + tl.lengthOfNewLineChars +} diff --git a/tools/text/text-range.go b/tools/text/text-range.go new file mode 100644 index 000000000..a751cda6a --- /dev/null +++ b/tools/text/text-range.go @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +import "fmt" + +// TextRange describes a contiguous sequence of unicode code points in the TextDocument. +type TextRange interface { + StartOffset() int + EndOffset() int + Length() int + Contains(position int) bool + IntersectionExists(textRange TextRange) bool + String() string + TextRangeLookupKey() TextRangeLookupKey +} + +// TextRangeLookupKey represents the comparable fields of TextRange for equality/hashing. +type TextRangeLookupKey struct { + StartOffset int + EndOffset int +} + +type textRangeImpl struct { + startOffset int + endOffset int + length int +} + +func TextRangeFromStartOffsetAndLength(startOffset, length int) TextRange { + return &textRangeImpl{ + startOffset: startOffset, + length: length, + endOffset: startOffset + length, + } +} + +func (tr textRangeImpl) StartOffset() int { + return tr.startOffset +} + +func (tr textRangeImpl) EndOffset() int { + return tr.endOffset +} + +func (tr textRangeImpl) Length() int { + return tr.length +} + +func (tr textRangeImpl) Contains(position int) bool { + return tr.startOffset <= position && position < tr.endOffset +} + +// IntersectionExists tests whether there exists an intersection of this range and the given range. +// The ranges R1(S1, E1) and R2(S2, E2) intersects if S1 is greater than or equal to E2 and +// S2 is less than or equal to E1. +func (tr textRangeImpl) IntersectionExists(textRange TextRange) bool { + return tr.startOffset <= textRange.EndOffset() && textRange.StartOffset() <= tr.endOffset +} + +func (tr textRangeImpl) String() string { + return fmt.Sprintf("(%d,%d)", tr.startOffset, tr.endOffset) +} + +// TextRangeLookupKey returns the lookup key for equality comparisons. +func (tr textRangeImpl) TextRangeLookupKey() TextRangeLookupKey { + return TextRangeLookupKey{ + StartOffset: tr.startOffset, + EndOffset: tr.endOffset, + } +}