1
0
Fork 0

Adding upstream version 2.6.3.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-18 22:58:26 +02:00
parent 4d8cd0ce4c
commit 2b08a89310
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
39 changed files with 2140 additions and 0 deletions

23
.editorconfig Normal file
View file

@ -0,0 +1,23 @@
; https://editorconfig.org/
root = true
[*]
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[{Makefile,go.mod,go.sum,*.go,.gitmodules}]
indent_style = tab
indent_size = 4
[*.md]
indent_size = 4
trim_trailing_whitespace = false
eclint_indent_style = unset
[Dockerfile]
indent_size = 4

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
* text=auto

13
.github/actions/core-test/Dockerfile vendored Normal file
View file

@ -0,0 +1,13 @@
FROM golang:bullseye
SHELL [ "/bin/bash", "-x", "-e", "-c" ]
ARG DEBIAN_FRONTEND=noninteraactive
RUN apt-get update -y && \
apt-get install -qy --no-install-recommends \
cmake \
git \
make && \
git config --system --add safe.directory '*'
CMD [ "make", "test-core", "test-skipped" ]

5
.github/actions/core-test/action.yml vendored Normal file
View file

@ -0,0 +1,5 @@
name: Core test
description: Run the editorconfig-core-test suite
runs:
using: docker
image: Dockerfile

11
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: gomod
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly

27
.github/workflows/golangci-lint.yml vendored Normal file
View file

@ -0,0 +1,27 @@
name: golangci-lint
on:
push:
tags:
- v*
branches:
- master
- main
pull_request:
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.23.x
- name: golangci-lint
uses: golangci/golangci-lint-action@2226d7cb06a077cd73e56eedd38eecad18e5d837 #v6.5.0
env:
GOTOOLCHAIN: local
with:
version: v1.64

32
.github/workflows/main.yml vendored Normal file
View file

@ -0,0 +1,32 @@
on: [push]
name: test and build
jobs:
editorconfig_lint:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4
- name: eclint-action
uses: greut/eclint-action@v0
go-test:
runs-on: ubuntu-latest
name: test
steps:
- name: checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.22.x
- name: go test
run: go test -v ./...
core-test:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4
with:
submodules: true
- name: core test
uses: ./.github/actions/core-test

20
.github/workflows/semgrep.yml vendored Normal file
View file

@ -0,0 +1,20 @@
# Name of this GitHub Actions workflow.
name: Semgrep
on:
pull_request: {}
jobs:
semgrep:
name: Scan
runs-on: ubuntu-latest
container:
image: returntocorp/semgrep
# Skip any PR created by dependabot to avoid permission issues
if: (github.actor != 'dependabot[bot]')
steps:
- uses: actions/checkout@v4
- run: semgrep ci
env:
SEMGREP_RULES: p/default p/golang p/secrets

29
.gitignore vendored Normal file
View file

@ -0,0 +1,29 @@
# ---> Go
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
# EditorConfig
/editorconfig

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "core-test"]
path = core-test
url = https://github.com/editorconfig/editorconfig-core-test.git

40
.golangci.yml Normal file
View file

@ -0,0 +1,40 @@
linters-settings:
depguard:
rules:
main:
files:
- "!**/internal/**/*.go"
- "!$test"
allow:
- $gostd
- "github.com/editorconfig/editorconfig-core-go/v2"
- "github.com/hashicorp/go-multierror"
- "golang.org/x/mod/semver"
- "gopkg.in/ini.v1"
deny: []
internal:
files:
- "**/internal/**/*.go"
- "!$test"
allow:
- $gostd
- "github.com/google/go-cmp"
gci:
sections:
- standard
- default
- prefix(github.com/editorconfig/editorconfig-core-go)
cyclop:
max-complexity: 15
package-average: 10
linters:
enable-all: true
disable:
- exhaustruct
- mnd
- tagliatelle
- tenv
- typecheck
- varnamelen
fast: false

76
.goreleaser.yml Normal file
View file

@ -0,0 +1,76 @@
version: 2
project_name: editorconfig-core-go
before:
hooks:
- go mod tidy
builds:
- id: editorconfig
main: ./cmd/editorconfig/main.go
binary: editorconfig
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
archives:
- id: tarball
builds:
- editorconfig
format_overrides:
- goos: windows
formats:
- zip
files:
- none*
dockers:
- image_templates:
- ghcr.io/editorconfig/editorconfig-core-go/editorconfig:latest
- ghcr.io/editorconfig/editorconfig-core-go/editorconfig:{{ .Tag }}
- ghcr.io/editorconfig/editorconfig-core-go/editorconfig:v{{ .Major }}
- ghcr.io/editorconfig/editorconfig-core-go/editorconfig:v{{ .Major }}.{{ .Minor }}
goos: linux
goarch: amd64
ids:
- editorconfig
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.version={{ .Version }}"
- "--label=org.opencontainers.image.title={{ .ProjectName }}"
nfpms:
- vendor: EditorConfig
homepage: https://github.com/editorconfig/editorconfig-core-go
maintainer: Yoan Blanc <yoan@dosimple.ch>
formats:
- deb
- rpm
epoch: 1
release: 1
checksum:
name_template: 'checksums.txt'
signs:
- artifacts: checksum
snapshot:
version_template: "{{ .Tag }}-development"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
release:
github:
owner: editorconfig
name: editorconfig-core-go
draft: true

229
CHANGELOG.md Normal file
View file

@ -0,0 +1,229 @@
# Change log
## v2.6.3 - 2025-03-12
- Targets Go 1.22
- Bump x/mod to 0.23
- Bump google/go-cmp to 0.7.0
## v2.6.2 - 2024-04-02
- Fix paths on Windows
- Bump golangci-lint to 1.56
- Bump x/mod to 0.16.0
## v2.6.1 - 2024-01-29
- Fix utf-8-bom contains a dash
- Bump google/go-cmp to 0.6.0
- Bump x/mod to 0.14.0
## v2.6.0 - 2023-09-27
- Fix path matching on Windows. The spec says that:
> Backslashes (`\\`) are not allowed as path separators (even on Windows).
- Replace go-multierror with Go 1.20 errors.Join
## v2.5.2 - 2023-04-19
- Bump golang.org/x/mod from 0.5.1 to 0.10.0
## v2.5.1 - 2022-10-02
- Fix typo in new method
## v2.5.0 - 2022-10-01
- Feature add a graceful parser that reports errors in the .editorconfig file as warnings
- Bump Go version to 1.18 in the go.mod
- Bump go.pkg.in/ini.v1 to 1.67.0
- Bump google/go-cmp to 0.5.9
## v2.4.5 - 2022-06-18
- Bump Go version to 1.17 in the go.mod
- Upgrade go.pkg.in/ini.v1 from 1.66.4 to 1.66.6
- Upgrade github.com/google/go-cmp from 0.5.7 to 0.5.8
## v2.4.4 - 2022-03-17
- Upgrade gopkg.in/ini.v1 from 1.53 to 1.66.4;
- Upgrade github.com/google/go-cmp from 0.5.6 to 0.5.7;
- Upgrade golang.org/x/mod from 0.5.0 to 0.5.1.
## v2.4.3 - 2021-09-21
- Upgrade go-cmp v0.5.6
([#110](https://github.com/editorconfig/editorconfig-core-go/pull/110));
- Upgrade go-ini v1.63.2;
- Upgrade x/mod v0.5.0
([#111](https://github.com/editorconfig/editorconfig-core-go/pull/111));
- Fix problems spotted by golangci-lint
([#115](https://github.com/editorconfig/editorconfig-core-go/pull/115));
## v2.4.2 - 2021-03-21
- Upgrade google/go-cmp v0.5.5
([#105](https://github.com/editorconfig/editorconfig-core-go/pull/105));
- Upgrade x/mod v0.4.2
([#106](https://github.com/editorconfig/editorconfig-core-go/pull/106)).
## v2.4.1 - 2021-02-25
- Fix for Go 1.16 os.IsNotExist wrapping
([#102](https://github.com/editorconfig/editorconfig-core-go/pull/102)).
## v2.4.0 - 2021-02-22
- Fix new core-test
([#100](https://github.com/editorconfig/editorconfig-core-go/pull/100));
- Upgrade github CI versions
([#99](https://github.com/editorconfig/editorconfig-core-go/pull/99));
- Upgrade x/mod v0.4.1
([#98](https://github.com/editorconfig/editorconfig-core-go/pull/98));
- Fix goreleaser deprecations
([#97](https://github.com/editorconfig/editorconfig-core-go/pull/97)).
## v2.3.10 - 2021-02-05
- Upgrade core-test
([#93](https://github.com/editorconfig/editorconfig-core-go/pull/93));
- Upgrade x/mod v0.4.0
([#94](https://github.com/editorconfig/editorconfig-core-go/pull/94));
- Upgrade golangci-lint to v1.34
([#95](https://github.com/editorconfig/editorconfig-core-go/pull/95)).
## v2.3.9 - 2020-11-28
- Fix path separator on Windows
([#69](https://github.com/editorconfig/editorconfig-core-go/pull/69));
- Upgrade go-cmp v0.5.4
([#91](https://github.com/editorconfig/editorconfig-core-go/pull/91)).
## v2.3.8 - 2020-10-17
- Feat more tests
([#83](https://github.com/editorconfig/editorconfig-core-go/pull/83));
- Upgrade go-ini v1.61.0
([#84](https://github.com/editorconfig/editorconfig-core-go/pull/84));
- Upgrade go-ini v1.62.0
([#85](https://github.com/editorconfig/editorconfig-core-go/pull/85)).
## v2.3.7 - 2020-09-05
- Upgrade go-ini v1.60.2, and go-cmp v0.5.2
([#81](https://github.com/editorconfig/editorconfig-core-go/pull/81)).
## v2.3.6 - 2020-08-25
- Use goerr113 linter
([#77](https://github.com/editorconfig/editorconfig-core-go/pull/77));
- Upgrade go-ini v1.60.0
([#78](https://github.com/editorconfig/editorconfig-core-go/pull/78));
- Upgrade go-ini v1.60.1
([#79](https://github.com/editorconfig/editorconfig-core-go/pull/79)).
## v2.3.5 - 2020-08-20
- Upgrade go-cmp v0.5.1
([#73](https://github.com/editorconfig/editorconfig-core-go/pull/73));
- Replace custom GitHub Action with official GolangCI Lint
([#74](https://github.com/editorconfig/editorconfig-core-go/pull/74));
- Upgrade go-ini v1.58.0
([#75](https://github.com/editorconfig/editorconfig-core-go/pull/75)).
## v2.3.4 - 2020-06-22
- Wrap errors using Go 1.13 syntax
([#61](https://github.com/editorconfig/editorconfig-core-go/pull/61));
- Upgrade base Docker image
([#68](https://github.com/editorconfig/editorconfig-core-go/pull/68));
- Upgrade go-ini v1.57.0, go-cmp v0.5.0
([#70](https://github.com/editorconfig/editorconfig-core-go/pull/70)).
## v2.3.3 - 2020-05-19
- Using goreleaser
([#22](https://github.com/editorconfig/editorconfig-core-go/pull/22));
- Upgrade go-cmp, go-ini, x/mod
([#60](https://github.com/editorconfig/editorconfig-core-go/pull/65));
- Update CI actions
([#63](https://github.com/editorconfig/editorconfig-core-go/pull/63));
## v2.3.2 - 2020-04-21
- Upgrade go-ini v1.55.0
([#60](https://github.com/editorconfig/editorconfig-core-go/pull/60));
- Build on latest Go
([#54](https://github.com/editorconfig/editorconfig-core-go/pull/54));
- Use GitHub action instead of Travis CI
([#50](https://github.com/editorconfig/editorconfig-core-go/pull/50));
## v2.3.1 - 2020-03-16
- Use golang/x/mod/semver for semantic versioning checks
([#55](https://github.com/editorconfig/editorconfig-core-go/pull/55));
- Enable wsl (WhiteSpace linter)
([#56](https://github.com/editorconfig/editorconfig-core-go/pull/56));
- Replace testify dependency with Google's go-cmp
([#57](https://github.com/editorconfig/editorconfig-core-go/pull/57));
- Upgrade go-ini to v1.54.0
([#58](https://github.com/editorconfig/editorconfig-core-go/pull/58)).
## v2.3.0 - 2020-02-14
- Implement a cached `Parser` to allow getting the definition of many files
at once without re-reading the `.editorconfig` or parsing the _globbing_
expression more than once.
([#51](https://github.com/editorconfig/editorconfig-core-go/pull/51));
- Run golangci-lint on travis
([#26](https://github.com/editorconfig/editorconfig-core-go/pull/26)).
## v2.2.2 - 2020-01-19
- Bump core test to master
([#42](https://github.com/editorconfig/editorconfig-core-go/pull/42));
- Bugfix error mangled when reading a file which could create a panic
([#47](https://github.com/editorconfig/editorconfig-core-go/pull/47));
- Bugfix INI file generated would not show the correct value
([#46](https://github.com/editorconfig/editorconfig-core-go/pull/46)).
## v2.2.1 - 2019-11-10
- Implement pre 0.9.0 behavior
([#39](https://github.com/editorconfig/editorconfig-core-go/pull/39));
- Fix values inheritance (regression)
([#43](https://github.com/editorconfig/editorconfig-core-go/pull/43)).
## v2.2.0 - 2019-10-12
- Allow parsing from an `io.Reader`, effectively deprecating `ParseBytes`
by [@mvdan](https://github.com/mvdan)
([#32](https://github.com/editorconfig/editorconfig-core-go/pull/32));
- Add support for the special `unset` value by [@greut](https://github.com/greut)
([#19](https://github.com/editorconfig/editorconfig-core-go/pull/19));
- Skip values, properties or section that are considered too long
([#35](https://github.com/editorconfig/editorconfig-core-go/pull/35));
- Clean up and documentation work by [@mstruebing](https://github.com/mstruebing/)
([#23](https://github.com/editorconfig/editorconfig-core-go/pull/23),
[#24](https://github.com/editorconfig/editorconfig-core-go/pull/24)).
## v2.1.1 - 2019-08-18
- Fix a small path bug
([#17](https://github.com/editorconfig/editorconfig-core-go/issues/17),
[#18](https://github.com/editorconfig/editorconfig-core-go/pull/18)).
## v2.1.0 - 2019-08-10
- This package is now *way* more compliant with the Editorconfig definition
thanks to a refactor work made by [@greut](https://github.com/greut)
([#15](https://github.com/editorconfig/editorconfig-core-go/pull/15)).
## v2.0.0 - 2019-07-14
- This project now uses [Go Modules](https://blog.golang.org/using-go-modules)
([#14](https://github.com/editorconfig/editorconfig-core-go/pull/14));
- The import path has been changed from `gopkg.in/editorconfig/editorconfig-core-go.v1`
to `github.com/editorconfig/editorconfig-core-go/v2`.

5
CMakeLists.txt Normal file
View file

@ -0,0 +1,5 @@
cmake_minimum_required(VERSION 3.13)
project(editorconfig-core-go)
enable_testing()
set(EDITORCONFIG_CMD ${CMAKE_CURRENT_LIST_DIR}/editorconfig)
add_subdirectory(core-test)

5
Dockerfile Normal file
View file

@ -0,0 +1,5 @@
FROM alpine:3.21
COPY editorconfig /usr/local/bin/
ENTRYPOINT [ "editorconfig" ]

8
LICENSE Normal file
View file

@ -0,0 +1,8 @@
MIT License
Copyright (c) 2016 The Editorconfig Team
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

35
Makefile Normal file
View file

@ -0,0 +1,35 @@
PROJECT_ROOT_DIR := $(CURDIR)
SRC := $(shell git ls-files *.go */*.go)
.PHONY: bin test test-go test-core test-skipped submodule
test: test-go test-core
submodule:
git submodule update --init
editorconfig: $(SRC)
go build \
-ldflags "-X main.version=1.99.99" \
github.com/editorconfig/editorconfig-core-go/v2/cmd/editorconfig
test-go:
go test -v ./...
test-core: editorconfig
cd core-test; \
cmake ..
cd core-test; \
ctest \
-E "^(octothorpe_in_value|(backslashed_)*semicolon_or_hash_in_property)$$" \
--output-on-failure \
.
test-skipped: editorconfig
cd core-test; \
cmake ..
cd core-test; \
ctest \
-R "^(octothorpe_in_value)$$" \
--show-only \
.

151
README.md Normal file
View file

@ -0,0 +1,151 @@
![Build Status](https://github.com/editorconfig/editorconfig-core-go/workflows/.github/workflows/main.yml/badge.svg)
[![Go Reference](https://pkg.go.dev/badge/github.com/editorconfig/editorconfig-core-go/v2.svg)](https://pkg.go.dev/github.com/editorconfig/editorconfig-core-go/v2)
[![Go Report Card](https://goreportcard.com/badge/github.com/editorconfig/editorconfig-core-go)](https://goreportcard.com/report/github.com/editorconfig/editorconfig-core-go)
# Editorconfig Core Go
A [Editorconfig][editorconfig] file parser and manipulator for Go.
## Missing features
- escaping comments in values, probably in [go-ini/ini](https://github.com/go-ini/ini)
- [adjacent nested braces](https://github.com/editorconfig/editorconfig-core-test/pull/44)
## Installing
We recommend the use of Go 1.17+ modules for this package. Lower versions, such as 1.13, should be fine.
Import by the same path. The package name you will use to access it is
`editorconfig`.
```go
import "github.com/editorconfig/editorconfig-core-go/v2"
```
## Usage
### Parse from a file
```go
fp, err := os.Open("path/to/.editorconfig")
if err != nil {
log.Fatal(err)
}
defer fp.Close()
editorConfig, err := editorconfig.Parse(fp)
if err != nil {
log.Fatal(err)
}
```
### Graceful parsing from a file
```go
fp, err := os.Open("path/to/.editorconfig")
if err != nil {
log.Fatal(err)
}
defer fp.Close()
editorConfig, warning, err := editorconfig.ParseGraceful(fp)
if err != nil {
log.Fatal(err)
}
// Log the warning(s) encountered while reading the editorconfig file
if warning != nil {
log.Print(warning)
}
```
### Parse from slice of bytes
```go
data := []byte("...")
editorConfig, err := editorconfig.ParseBytes(data)
if err != nil {
log.Fatal(err)
}
```
### Get definition to a given filename
This method builds a definition to a given filename.
This definition is a merge of the properties with selectors that matched the
given filename.
The lasts sections of the file have preference over the priors.
```go
def := editorConfig.GetDefinitionForFilename("my/file.go")
```
This definition have the following properties:
```go
type Definition struct {
Selector string
Charset string
IndentStyle string
IndentSize string
TabWidth int
EndOfLine string
TrimTrailingWhitespace *bool
InsertFinalNewline *bool
Raw map[string]string
}
```
#### Automatic search for `.editorconfig` files
If you want a definition of a file without having to manually
parse the `.editorconfig` files, you can then use the static version
of `GetDefinitionForFilename`:
```go
def, err := editorconfig.GetDefinitionForFilename("foo/bar/baz/my-file.go")
```
In the example above, the package will automatically search for
`.editorconfig` files on:
- `foo/bar/baz/.editorconfig`
- `foo/baz/.editorconfig`
- `foo/.editorconfig`
Until it reaches a file with `root = true` or the root of the filesystem.
### Generating a .editorconfig file
You can easily convert a Editorconfig struct to a compatible INI file:
```go
// serialize to slice of bytes
data, err := editorConfig.Serialize()
if err != nil {
log.Fatal(err)
}
// save directly to file
err := editorConfig.Save("path/to/.editorconfig")
if err != nil {
log.Fatal(err)
}
```
## Contributing
To run the tests:
```bash
go test -v ./...
```
To run the [integration tests](https://github.com/editorconfig/editorconfig-core-test):
```bash
make test-core
```
[editorconfig]: https://editorconfig.org/

89
cached_parser.go Normal file
View file

@ -0,0 +1,89 @@
package editorconfig
import (
"errors"
"fmt"
"os"
"regexp"
"gopkg.in/ini.v1"
)
// CachedParser implements the Parser interface but caches the definition and
// the regular expressions.
type CachedParser struct {
editorconfigs map[string]*Editorconfig
regexps map[string]*regexp.Regexp
}
// NewCachedParser initializes the CachedParser.
func NewCachedParser() *CachedParser {
return &CachedParser{
editorconfigs: make(map[string]*Editorconfig),
regexps: make(map[string]*regexp.Regexp),
}
}
// ParseIni parses the given filename to a Definition and caches the result.
func (parser *CachedParser) ParseIni(filename string) (*Editorconfig, error) {
ec, warning, err := parser.ParseIniGraceful(filename)
if err != nil {
return nil, err
}
return ec, warning
}
// ParseIniGraceful parses the given filename to a Definition and caches the result.
func (parser *CachedParser) ParseIniGraceful(filename string) (*Editorconfig, error, error) {
var warning error
ec, ok := parser.editorconfigs[filename]
if !ok {
fp, err := os.Open(filename)
if err != nil {
return nil, nil, fmt.Errorf("error opening %q: %w", filename, err)
}
defer fp.Close()
iniFile, err := ini.Load(fp)
if err != nil {
return nil, nil, fmt.Errorf("error loading ini file %q: %w", filename, err)
}
var warn error
ec, warn, err = newEditorconfig(iniFile)
if err != nil {
return nil, nil, fmt.Errorf("error creating config: %w", err)
}
if warn != nil {
warning = errors.Join(warning, warn)
}
parser.editorconfigs[filename] = ec
}
return ec, warning, nil
}
// FnmatchCase calls the module's FnmatchCase and caches the parsed selector.
func (parser *CachedParser) FnmatchCase(selector string, filename string) (bool, error) {
r, ok := parser.regexps[selector]
if !ok {
p := translate(selector)
var err error
r, err = regexp.Compile(fmt.Sprintf("^%s$", p))
if err != nil {
return false, fmt.Errorf("error compiling selector %q: %w", selector, err)
}
parser.regexps[selector] = r
}
return r.MatchString(filename), nil
}

74
cmd/editorconfig/main.go Normal file
View file

@ -0,0 +1,74 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"gopkg.in/ini.v1"
"github.com/editorconfig/editorconfig-core-go/v2"
)
// version indicates the current version number.
var version = "dev"
func main() {
var (
configName string
configVersion string
showVersionFlag bool
)
flag.StringVar(&configName, "f", editorconfig.ConfigNameDefault, "Specify conf filename other than '.editorconfig'")
flag.StringVar(&configVersion, "b", "", "Specify version (used by devs to test compatibility)")
flag.BoolVar(&showVersionFlag, "v", false, "Display version information")
flag.BoolVar(&showVersionFlag, "version", false, "Display version information")
flag.Parse()
if showVersionFlag {
fmt.Printf("EditorConfig Core Go, Version %s\n", version) //nolint:forbidigo
os.Exit(0)
}
rest := flag.Args()
if len(rest) < 1 {
flag.Usage()
os.Exit(1)
}
config := &editorconfig.Config{
Name: configName,
Version: configVersion,
Graceful: false,
}
if len(rest) > 1 {
config.Parser = editorconfig.NewCachedParser()
}
for _, file := range rest {
def, err := config.Load(file)
if err != nil {
log.Fatal(err)
}
iniFile := ini.Empty()
ini.PrettyFormat = false
if len(rest) < 2 {
def.Selector = ini.DefaultSection
} else {
def.Selector = file
}
def.InsertToIniFile(iniFile)
_, err = iniFile.WriteTo(os.Stdout)
if err != nil {
log.Fatal(err)
}
}
}

111
config.go Normal file
View file

@ -0,0 +1,111 @@
package editorconfig
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"golang.org/x/mod/semver"
)
// ErrInvalidVersion represents a standard error with the semantic version.
var ErrInvalidVersion = errors.New("invalid semantic version")
// Config holds the configuration.
type Config struct {
Path string
Name string
Version string
Parser Parser
Graceful bool
}
// Load loads definition of a given file.
func (config *Config) Load(filename string) (*Definition, error) {
definition, warning, err := config.LoadGraceful(filename)
if warning != nil {
err = errors.Join(err, warning)
}
return definition, err
}
// Load loads definition of a given file with warnings and error.
func (config *Config) LoadGraceful(filename string) (*Definition, error, error) { //nolint:funlen
// idiomatic go allows empty struct
if config.Parser == nil {
config.Parser = new(SimpleParser)
}
absFilename, err := filepath.Abs(filename)
if err != nil {
return nil, nil, fmt.Errorf("cannot get absolute path for %q: %w", filename, err)
}
ecFile := config.Name
if ecFile == "" {
ecFile = ConfigNameDefault
}
definition := &Definition{}
definition.Raw = make(map[string]string)
if config.Version != "" {
version := config.Version
if !strings.HasPrefix(version, "v") {
version = "v" + version
}
if ok := semver.IsValid(version); !ok {
return nil, nil, fmt.Errorf("version %s error: %w", config.Version, ErrInvalidVersion)
}
definition.version = version
}
var warning error
dir := absFilename
for dir != filepath.Dir(dir) {
dir = filepath.Dir(dir)
ec, warn, err := config.Parser.ParseIniGraceful(filepath.Join(dir, ecFile))
if warn != nil {
warning = errors.Join(warning, warn)
}
if err != nil {
if errors.Is(err, os.ErrNotExist) {
continue
}
return nil, nil, fmt.Errorf("cannot parse the ini file %q: %w", ecFile, err)
}
// give it the current config.
ec.config = config
relativeFilename := absFilename
if len(dir) < len(relativeFilename) {
relativeFilename = relativeFilename[len(dir):]
}
// turn any Windows-y filename into the standard forward slash ones.
relativeFilename = filepath.ToSlash(relativeFilename)
def, err := ec.GetDefinitionForFilename(relativeFilename)
if err != nil {
return nil, nil, fmt.Errorf("cannot get definition for %q: %w", relativeFilename, err)
}
definition.merge(def)
if ec.Root {
break
}
}
return definition, warning, nil
}

203
definition.go Normal file
View file

@ -0,0 +1,203 @@
package editorconfig
import (
"errors"
"fmt"
"strconv"
"strings"
"golang.org/x/mod/semver"
"gopkg.in/ini.v1"
)
// Definition represents a definition inside the .editorconfig file.
// E.g. a section of the file.
// The definition is composed of the selector ("*", "*.go", "*.{js.css}", etc),
// plus the properties of the selected files.
type Definition struct {
Selector string `ini:"-" json:"-"`
Charset string `ini:"charset" json:"charset,omitempty"`
IndentStyle string `ini:"indent_style" json:"indent_style,omitempty"`
IndentSize string `ini:"indent_size" json:"indent_size,omitempty"`
TabWidth int `ini:"-" json:"-"`
EndOfLine string `ini:"end_of_line" json:"end_of_line,omitempty"`
TrimTrailingWhitespace *bool `ini:"-" json:"-"`
InsertFinalNewline *bool `ini:"-" json:"-"`
Raw map[string]string `ini:"-" json:"-"`
version string
}
// NewDefinition builds a definition from a given config.
func NewDefinition(config Config) (*Definition, error) {
return config.Load(config.Path)
}
// normalize fixes some values to their lowercase value.
func (d *Definition) normalize() error {
var result error
d.Charset = strings.ToLower(d.Charset)
d.EndOfLine = strings.ToLower(d.Raw["end_of_line"])
d.IndentStyle = strings.ToLower(d.Raw["indent_style"])
trimTrailingWhitespace, ok := d.Raw["trim_trailing_whitespace"]
if ok && trimTrailingWhitespace != UnsetValue {
trim, err := strconv.ParseBool(trimTrailingWhitespace)
if err != nil {
result = errors.Join(
result,
fmt.Errorf("trim_trailing_whitespace=%s is not an acceptable value. %w", trimTrailingWhitespace, err),
)
} else {
d.TrimTrailingWhitespace = &trim
}
}
insertFinalNewline, ok := d.Raw["insert_final_newline"]
if ok && insertFinalNewline != UnsetValue {
insert, err := strconv.ParseBool(insertFinalNewline)
if err != nil {
result = errors.Join(
result,
fmt.Errorf("insert_final_newline=%s is not an acceptable value. %w", insertFinalNewline, err),
)
} else {
d.InsertFinalNewline = &insert
}
}
// tab_width from Raw
tabWidth, ok := d.Raw["tab_width"]
if ok && tabWidth != UnsetValue {
num, err := strconv.Atoi(tabWidth)
if err != nil {
result = errors.Join(result, fmt.Errorf("tab_width=%s is not an acceptable value. %w", tabWidth, err))
} else {
d.TabWidth = num
}
}
// tab_width defaults to indent_size:
// https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#tab_width
num, err := strconv.Atoi(d.IndentSize)
if err == nil && d.TabWidth <= 0 {
d.TabWidth = num
}
return result
}
// merge the parent definition into the child definition.
func (d *Definition) merge(md *Definition) {
if len(d.Charset) == 0 {
d.Charset = md.Charset
}
if len(d.IndentStyle) == 0 {
d.IndentStyle = md.IndentStyle
}
if len(d.IndentSize) == 0 {
d.IndentSize = md.IndentSize
}
if d.TabWidth <= 0 {
d.TabWidth = md.TabWidth
}
if len(d.EndOfLine) == 0 {
d.EndOfLine = md.EndOfLine
}
if trimTrailingWhitespace, ok := d.Raw["trim_trailing_whitespace"]; !ok || trimTrailingWhitespace != UnsetValue {
if d.TrimTrailingWhitespace == nil {
d.TrimTrailingWhitespace = md.TrimTrailingWhitespace
}
}
if insertFinalNewline, ok := d.Raw["insert_final_newline"]; !ok || insertFinalNewline != UnsetValue {
if d.InsertFinalNewline == nil {
d.InsertFinalNewline = md.InsertFinalNewline
}
}
for k, v := range md.Raw {
if _, ok := d.Raw[k]; !ok {
d.Raw[k] = v
}
}
}
// InsertToIniFile writes the definition into a ini file.
func (d *Definition) InsertToIniFile(iniFile *ini.File) { //nolint:funlen,gocognit,cyclop
iniSec := iniFile.Section(d.Selector)
for k, v := range d.Raw {
switch k {
case "insert_final_newline":
if d.InsertFinalNewline != nil {
v = strconv.FormatBool(*d.InsertFinalNewline)
} else {
insertFinalNewline, ok := d.Raw["insert_final_newline"]
if !ok {
break
}
v = strings.ToLower(insertFinalNewline)
}
case "trim_trailing_whitespace":
if d.TrimTrailingWhitespace != nil {
v = strconv.FormatBool(*d.TrimTrailingWhitespace)
} else {
trimTrailingWhitespace, ok := d.Raw["trim_trailing_whitespace"]
if !ok {
break
}
v = strings.ToLower(trimTrailingWhitespace)
}
case "charset":
v = d.Charset
case "end_of_line":
v = d.EndOfLine
case "indent_style":
v = d.IndentStyle
case "tab_width":
tabWidth, ok := d.Raw["tab_width"]
if ok && tabWidth == UnsetValue {
v = tabWidth
} else {
v = strconv.Itoa(d.TabWidth)
}
case "indent_size":
v = d.IndentSize
}
iniSec.NewKey(k, v) //nolint:errcheck
}
if _, ok := d.Raw["indent_size"]; !ok {
tabWidth, ok := d.Raw["tab_width"]
switch {
case ok && tabWidth == UnsetValue:
// do nothing
case d.TabWidth > 0:
iniSec.NewKey("indent_size", strconv.Itoa(d.TabWidth)) //nolint:errcheck
case d.IndentStyle == IndentStyleTab && (d.version == "" || semver.Compare(d.version, "v0.9.0") >= 0):
iniSec.NewKey("indent_size", IndentStyleTab) //nolint:errcheck
}
}
if _, ok := d.Raw["tab_width"]; !ok {
if d.IndentSize == UnsetValue {
iniSec.NewKey("tab_width", d.IndentSize) //nolint:errcheck
} else {
_, err := strconv.Atoi(d.IndentSize)
if err == nil {
iniSec.NewKey("tab_width", d.Raw["indent_size"]) //nolint:errcheck
}
}
}
}

310
editorconfig.go Normal file
View file

@ -0,0 +1,310 @@
package editorconfig
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"strings"
"gopkg.in/ini.v1"
)
const (
// ConfigNameDefault represents the name of the configuration file.
ConfigNameDefault = ".editorconfig"
// UnsetValue is the value that unsets a preexisting variable.
UnsetValue = "unset"
)
// IndentStyle possible values.
const (
IndentStyleTab = "tab"
IndentStyleSpaces = "space"
)
// EndOfLine possible values.
const (
EndOfLineLf = "lf"
EndOfLineCr = "cr"
EndOfLineCrLf = "crlf"
)
// Charset possible values.
const (
CharsetLatin1 = "latin1"
CharsetUTF8 = "utf-8"
CharsetUTF8BOM = "utf-8-bom"
CharsetUTF16BE = "utf-16be"
CharsetUTF16LE = "utf-16le"
)
// Limit for section name.
const (
MaxSectionLength = 4096
)
// Editorconfig represents a .editorconfig file.
//
// It is composed by a "root" property, plus the definitions defined in the
// file.
type Editorconfig struct {
Root bool
Definitions []*Definition
config *Config
}
// newEditorconfig builds the configuration from an INI file.
func newEditorconfig(iniFile *ini.File) (*Editorconfig, error, error) {
editorConfig := &Editorconfig{}
var warning error
// Consider mixed-case values for true and false.
rootKey := iniFile.Section(ini.DefaultSection).Key("root")
rootKey.SetValue(strings.ToLower(rootKey.Value()))
editorConfig.Root = rootKey.MustBool(false)
for _, sectionStr := range iniFile.SectionStrings() {
if sectionStr == ini.DefaultSection || len(sectionStr) > MaxSectionLength {
continue
}
iniSection := iniFile.Section(sectionStr)
definition := &Definition{}
raw := make(map[string]string)
if err := iniSection.MapTo(&definition); err != nil {
return nil, nil, fmt.Errorf("error mapping current section: %w", err)
}
// Shallow copy all the properties
for k, v := range iniSection.KeysHash() {
raw[strings.ToLower(k)] = v
}
definition.Raw = raw
definition.Selector = sectionStr
if err := definition.normalize(); err != nil {
// Append those error(s) into the warning
warning = errors.Join(warning, err)
}
editorConfig.Definitions = append(editorConfig.Definitions, definition)
}
return editorConfig, warning, nil
}
// GetDefinitionForFilename returns a definition for the given filename.
//
// The result is a merge of the selectors that matched the file.
// The last section has preference over the priors.
func (e *Editorconfig) GetDefinitionForFilename(name string) (*Definition, error) {
def := &Definition{
Raw: make(map[string]string),
}
// The last section has preference over the priors.
for i := len(e.Definitions) - 1; i >= 0; i-- {
actualDef := e.Definitions[i]
selector := actualDef.Selector
if !strings.HasPrefix(selector, "/") {
if strings.ContainsRune(selector, '/') {
selector = "/" + selector
} else {
selector = "/**/" + selector
}
}
if !strings.HasPrefix(name, "/") {
name = "/" + name
}
ok, err := e.FnmatchCase(selector, name)
if err != nil {
return nil, err
}
if ok {
def.merge(actualDef)
}
}
return def, nil
}
// FnmatchCase calls the matcher from the config's parser or the vanilla's.
func (e *Editorconfig) FnmatchCase(selector string, filename string) (bool, error) {
if e.config != nil && e.config.Parser != nil {
ok, err := e.config.Parser.FnmatchCase(selector, filename)
if err != nil {
return ok, fmt.Errorf("filename match failed: %w", err)
}
return ok, nil
}
return FnmatchCase(selector, filename)
}
// Serialize converts the Editorconfig to a slice of bytes, containing the
// content of the file in the INI format.
func (e *Editorconfig) Serialize() ([]byte, error) {
buffer := bytes.NewBuffer(nil)
if err := e.Write(buffer); err != nil {
return nil, fmt.Errorf("cannot write into buffer: %w", err)
}
return buffer.Bytes(), nil
}
// Write writes the Editorconfig to the Writer in a compatible INI file.
func (e *Editorconfig) Write(w io.Writer) error {
iniFile := ini.Empty()
iniFile.Section(ini.DefaultSection).Comment = "https://editorconfig.org"
if e.Root {
iniFile.Section(ini.DefaultSection).Key("root").SetValue(boolToString(e.Root))
}
for _, d := range e.Definitions {
d.InsertToIniFile(iniFile)
}
if _, err := iniFile.WriteTo(w); err != nil {
return fmt.Errorf("error writing ini file: %w", err)
}
return nil
}
// Save saves the Editorconfig to a compatible INI file.
func (e *Editorconfig) Save(filename string) error {
f, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
if err != nil {
return fmt.Errorf("cannot open file %q: %w", filename, err)
}
return e.Write(f)
}
func boolToString(b bool) string {
if b {
return "true"
}
return "false"
}
// Parse parses from a reader.
func Parse(r io.Reader) (*Editorconfig, error) {
iniFile, err := ini.Load(r)
if err != nil {
return nil, fmt.Errorf("cannot load ini file: %w", err)
}
ec, warning, err := newEditorconfig(iniFile)
if warning != nil {
err = errors.Join(warning, err)
}
return ec, err
}
// ParseGraceful parses from a reader with warnings not treated as a fatal error.
func ParseGraceful(r io.Reader) (*Editorconfig, error, error) {
iniFile, err := ini.Load(r)
if err != nil {
return nil, nil, fmt.Errorf("cannot load ini file: %w", err)
}
return newEditorconfig(iniFile)
}
// ParseBytes parses from a slice of bytes.
//
// Deprecated: use Parse instead.
func ParseBytes(data []byte) (*Editorconfig, error) {
iniFile, err := ini.Load(data)
if err != nil {
return nil, fmt.Errorf("cannot load ini file: %w", err)
}
ec, warning, err := newEditorconfig(iniFile)
if warning != nil {
err = errors.Join(warning, err)
}
return ec, err
}
// ParseFile parses from a file.
//
// Deprecated: use Parse instead.
func ParseFile(path string) (*Editorconfig, error) {
iniFile, err := ini.Load(path)
if err != nil {
return nil, fmt.Errorf("cannot load ini file: %w", err)
}
ec, warning, err := newEditorconfig(iniFile)
if warning != nil {
err = errors.Join(warning, err)
}
return ec, err
}
// GetDefinitionForFilename given a filename, searches for .editorconfig files,
// starting from the file folder, walking through the previous folders, until
// it reaches a folder with `root = true`, and returns the right editorconfig
// definition for the given file.
func GetDefinitionForFilename(filename string) (*Definition, error) {
config := new(Config)
return config.Load(filename)
}
// GetDefinitionForFilenameGraceful given a filename, searches for
// .editorconfig files, starting from the file folder, walking through the
// previous folders, until it reaches a folder with `root = true`, and returns
// the right editorconfig definition for the given file.
//
// In case of non-fatal errors, a joined errors warning is return as well.
func GetDefinitionForFilenameGraceful(filename string) (*Definition, error, error) {
config := new(Config)
return config.LoadGraceful(filename)
}
// GetDefinitionForFilenameWithConfigname given a filename and a configname,
// searches for configname files, starting from the file folder, walking
// through the previous folders, until it reaches a folder with `root = true`,
// and returns the right editorconfig definition for the given file.
func GetDefinitionForFilenameWithConfigname(filename string, configname string) (*Definition, error) {
config := &Config{
Name: configname,
}
return config.Load(filename)
}
// GetDefinitionForFilenameWithConfignameGraceful given a filename and a
// configname, searches for configname files, starting from the file folder,
// walking through the previous folders, until it reaches a folder with `root =
// true`, and returns the right editorconfig definition for the given file.
//
// In case of non-fatal errors, a joined errors warning is return as well.
func GetDefinitionForFilenameWithConfignameGraceful(filename string, configname string) (*Definition, error, error) {
config := &Config{
Name: configname,
}
return config.LoadGraceful(filename)
}

179
editorconfig_test.go Normal file
View file

@ -0,0 +1,179 @@
package editorconfig //nolint:testpackage
import (
"errors"
"os"
"path/filepath"
"strings"
"testing"
"testing/iotest"
"github.com/editorconfig/editorconfig-core-go/v2/internal/assert"
)
const (
testFile = "testdata/.editorconfig"
)
func testParse(t *testing.T, ec *Editorconfig) {
t.Helper()
assert.Equal(t, true, ec.Root)
assert.Equal(t, 3, len(ec.Definitions))
def := ec.Definitions[0]
assert.Equal(t, "*", def.Selector)
assert.Equal(t, EndOfLineLf, def.EndOfLine)
assert.Equal(t, true, *def.InsertFinalNewline)
assert.Equal(t, CharsetUTF8, def.Charset)
assert.Equal(t, true, *def.TrimTrailingWhitespace)
assert.Equal(t, "8", def.IndentSize)
def = ec.Definitions[1]
assert.Equal(t, "*.go", def.Selector)
assert.Equal(t, IndentStyleTab, def.IndentStyle)
assert.Equal(t, "4", def.IndentSize)
assert.Equal(t, 4, def.TabWidth)
def = ec.Definitions[2]
assert.Equal(t, "*.{js,css,less,htm,html}", def.Selector)
assert.Equal(t, IndentStyleSpaces, def.IndentStyle)
assert.Equal(t, "2", def.IndentSize)
assert.Equal(t, 2, def.TabWidth)
}
func TestParseFile(t *testing.T) {
t.Parallel()
ec, err := ParseFile(testFile)
assert.Nil(t, err)
testParse(t, ec)
}
func TestParseBytes(t *testing.T) { //nolint:paralleltest
data, err := os.ReadFile(testFile)
assert.Nil(t, err)
ec, err := ParseBytes(data)
assert.Nil(t, err)
testParse(t, ec)
}
func TestParseReader(t *testing.T) { //nolint:paralleltest
f, err := os.Open(testFile)
assert.Nil(t, err)
defer f.Close()
ec, err := Parse(f)
assert.Nil(t, err)
testParse(t, ec)
}
func TestParseReaderTimeoutError(t *testing.T) { //nolint:paralleltest
f, err := os.Open(testFile)
assert.Nil(t, err)
defer f.Close()
_, err = Parse(iotest.TimeoutReader(f))
assert.Equal(t, true, errors.Is(err, iotest.ErrTimeout))
}
func TestGetDefinition(t *testing.T) {
t.Parallel()
ec, err := ParseFile(testFile)
if err != nil {
t.Errorf("Couldn't parse file: %v", err)
}
def, err := ec.GetDefinitionForFilename("main.go")
if err != nil {
t.Errorf("Couldn't parse file: %v", err)
}
assert.Equal(t, IndentStyleTab, def.IndentStyle)
assert.Equal(t, "4", def.IndentSize)
assert.Equal(t, 4, def.TabWidth)
assert.Equal(t, true, *def.TrimTrailingWhitespace)
assert.Equal(t, CharsetUTF8, def.Charset)
assert.Equal(t, true, *def.InsertFinalNewline)
assert.Equal(t, EndOfLineLf, def.EndOfLine)
}
func TestWrite(t *testing.T) { //nolint:paralleltest
ec, err := ParseFile(testFile)
if err != nil {
t.Errorf("Couldn't parse file: %v", err)
}
tempFile := filepath.Join(os.TempDir(), ".editorconfig")
f, err := os.OpenFile(tempFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
assert.Nil(t, err)
defer func() {
f.Close()
os.Remove(tempFile)
}()
err = ec.Write(f)
assert.Nil(t, err)
savedEc, err := ParseFile(tempFile)
assert.Nil(t, err)
testParse(t, savedEc)
}
func TestSave(t *testing.T) {
t.Parallel()
ec, err := ParseFile(testFile)
if err != nil {
t.Errorf("Couldn't parse file: %v", err)
}
tempFile := filepath.Join(os.TempDir(), ".editorconfig")
defer os.Remove(tempFile)
err = ec.Save(tempFile)
assert.Nil(t, err)
savedEc, err := ParseFile(tempFile)
assert.Nil(t, err)
testParse(t, savedEc)
}
func TestPublicTestDefinitionForFilename(t *testing.T) {
t.Parallel()
def, err := GetDefinitionForFilename("testdata/root/src/dummy.go")
assert.Nil(t, err)
assert.Equal(t, "4", def.IndentSize)
assert.Equal(t, IndentStyleTab, def.IndentStyle)
assert.Equal(t, true, *def.InsertFinalNewline)
assert.Equal(t, (*bool)(nil), def.TrimTrailingWhitespace)
}
func TestPublicTestDefinitionForFilenameWithConfigname(t *testing.T) {
t.Parallel()
def, warning, err := GetDefinitionForFilenameWithConfignameGraceful("testdata/root/src/dummy.go", "a.ini")
// Checking that we've got three warnings by splitting the lines
message := warning.Error()
merr := strings.Split(message, "\n")
assert.Equal(t, 3, len(merr))
assert.Nil(t, err)
assert.Equal(t, "5", def.IndentSize)
assert.Equal(t, IndentStyleSpaces, def.IndentStyle)
assert.Equal(t, false, *def.InsertFinalNewline)
assert.Equal(t, false, *def.TrimTrailingWhitespace)
}

213
fnmatch.go Normal file
View file

@ -0,0 +1,213 @@
package editorconfig
import (
"fmt"
"regexp"
"strconv"
"strings"
)
var (
// findLeftBrackets matches the opening left bracket {.
findLeftBrackets = regexp.MustCompile(`(^|[^\\])\{`)
// findDoubleLeftBrackets matches the duplicated opening left bracket {{.
findDoubleLeftBrackets = regexp.MustCompile(`(^|[^\\])\{\{`)
// findLeftBrackets matches the closing right bracket {.
findRightBrackets = regexp.MustCompile(`(^|[^\\])\}`)
// findDoubleRightBrackets matches the duplicated opening left bracket {{.
findDoubleRightBrackets = regexp.MustCompile(`(^|[^\\])\}\}`)
// findNumericRange matches a range of number, e.g. -2..5.
findNumericRange = regexp.MustCompile(`^([+-]?\d+)\.\.([+-]?\d+)$`)
)
// FnmatchCase tests whether the name matches the given pattern case included.
func FnmatchCase(pattern, name string) (bool, error) {
p := translate(pattern)
r, err := regexp.Compile(fmt.Sprintf("^%s$", p))
if err != nil {
return false, fmt.Errorf("error compiling %q: %w", pattern, err)
}
return r.MatchString(name), nil
}
func translate(pattern string) string { //nolint:funlen,gocognit,gocyclo,cyclop,maintidx
index := 0
pat := []rune(pattern)
length := len(pat)
result := strings.Builder{}
braceLevel := 0
isEscaped := false
inBrackets := false
// Double left and right is a hack to pass the core-test suite.
left := len(findLeftBrackets.FindAllString(pattern, -1))
doubleLeft := len(findDoubleLeftBrackets.FindAllString(pattern, -1))
right := len(findRightBrackets.FindAllString(pattern, -1))
doubleRight := len(findDoubleRightBrackets.FindAllString(pattern, -1))
matchesBraces := left+doubleLeft == right+doubleRight
pathSeparator := "/"
for index < length {
r := pat[index]
index++
switch r {
case '*':
p := index
if p < length && pat[p] == '*' {
result.WriteString(".*")
index++
} else {
result.WriteString(fmt.Sprintf("[^%s]*", pathSeparator))
}
case '/':
p := index
if p+2 < length && pat[p] == '*' && pat[p+1] == '*' && pat[p+2] == '/' {
result.WriteString(fmt.Sprintf("(?:%s|%s.*%s)", pathSeparator, pathSeparator, pathSeparator))
index += 3
} else {
result.WriteRune(r)
}
case '?':
result.WriteString(fmt.Sprintf("[^%s]", pathSeparator))
case '[':
if inBrackets { //nolint:nestif
result.WriteString("\\[")
} else {
hasSlash := false
res := strings.Builder{}
p := index
for p < length {
if pat[p] == ']' && pat[p-1] != '\\' {
break
}
res.WriteRune(pat[p])
if pat[p] == '/' && pat[p-1] != '\\' {
hasSlash = true
break
}
p++
}
if hasSlash {
result.WriteString("\\[" + res.String())
index = p + 1
} else {
if index < length && pat[index] == '!' || pat[index] == '^' {
result.WriteString("[^")
index++
} else {
result.WriteRune('[')
}
inBrackets = true
}
}
case ']':
if inBrackets && pat[index-2] == '\\' {
result.WriteString("\\]")
} else {
result.WriteRune(r)
inBrackets = false
}
case '{':
hasComma := false
p := index
res := strings.Builder{}
for p < length {
if pat[p] == '}' && pat[p-1] != '\\' {
break
}
res.WriteRune(pat[p])
if pat[p] == ',' && pat[p-1] != '\\' {
hasComma = true
break
}
p++
}
switch {
case !hasComma && p < length:
inner := res.String()
sub := findNumericRange.FindStringSubmatch(inner)
if len(sub) == 3 {
from, _ := strconv.Atoi(sub[1])
to, _ := strconv.Atoi(sub[2])
result.WriteString("(?:")
// XXX does not scale well
for i := from; i < to; i++ {
result.WriteString(strconv.Itoa(i))
result.WriteRune('|')
}
result.WriteString(strconv.Itoa(to))
result.WriteRune(')')
} else {
r := translate(inner)
result.WriteString(fmt.Sprintf("\\{%s\\}", r))
}
index = p + 1
case matchesBraces:
result.WriteString("(?:")
braceLevel++
default:
result.WriteString("\\{")
}
case '}':
if braceLevel > 0 {
if isEscaped {
result.WriteRune('}')
isEscaped = false
} else {
result.WriteRune(')')
braceLevel--
}
} else {
result.WriteString("\\}")
}
case ',':
if braceLevel == 0 || isEscaped {
result.WriteRune(r)
} else {
result.WriteRune('|')
}
default:
if r != '\\' || isEscaped {
result.WriteString(regexp.QuoteMeta(string(r)))
isEscaped = false
} else {
isEscaped = true
}
}
}
return result.String()
}

40
fnmatch_test.go Normal file
View file

@ -0,0 +1,40 @@
package editorconfig //nolint:testpackage
import (
"testing"
)
func TestTranslate(t *testing.T) {
t.Parallel()
tests := []struct {
pattern string
expected string
}{
{"a*e.c", `a[^/]*e\.c`},
{"a**z.c", `a.*z\.c`},
{"d/**/z.c", `d(?:/|/.*/)z\.c`},
{"som?.c", `som[^/]\.c`},
{"[\\]ab].g", `[\]ab]\.g`},
{"[ab]].g", `[ab]]\.g`},
{"ab[/c", `ab\[/c`},
{"*.{py,js,html}", `[^/]*\.(?:py|js|html)`},
{"{single}.b", `\{single\}\.b`},
{"{{,b,c{d}.i", `\{\{,b,c\{d\}\.i`},
{"{a\\,b,cd}", `(?:a,b|cd)`},
{"{e,\\},f}", `(?:e|}|f)`},
{"{a,{b,c}}", `(?:a|(?:b|c))`},
{"{{a,b},c}", `(?:(?:a|b)|c)`},
}
for _, test := range tests {
t.Run(test.pattern, func(t *testing.T) {
t.Parallel()
result := translate(test.pattern)
if result != test.expected {
t.Errorf("%s != %s", test.expected, result)
}
})
}
}

2
go.doc Normal file
View file

@ -0,0 +1,2 @@
// Package editorconfig can be used to parse and generate editorconfig files.
// For more information about editorconfig, see https://editorconfig.org/

13
go.mod Normal file
View file

@ -0,0 +1,13 @@
module github.com/editorconfig/editorconfig-core-go/v2
go 1.22.0
toolchain go1.23.0
require (
github.com/google/go-cmp v0.7.0
golang.org/x/mod v0.23.0
gopkg.in/ini.v1 v1.67.0
)
require github.com/stretchr/testify v1.7.0 // indirect

16
go.sum Normal file
View file

@ -0,0 +1,16 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

22
internal/assert/assert.go Normal file
View file

@ -0,0 +1,22 @@
package assert
import (
"testing"
"github.com/google/go-cmp/cmp"
)
func Equal(t *testing.T, x, y interface{}) {
t.Helper()
r := DiffReporter{}
if !cmp.Equal(x, y, cmp.Reporter(&r)) {
t.Error(r.String())
}
}
func Nil(t *testing.T, x interface{}) {
t.Helper()
Equal(t, x, nil)
}

View file

@ -0,0 +1,36 @@
// this code is from the examples
// https://pkg.go.dev/github.com/google/go-cmp/cmp?tab=doc#Reporter
package assert
import (
"fmt"
"strings"
"github.com/google/go-cmp/cmp"
)
// DiffReporter is a simple custom reporter that only records differences
// detected during comparison.
type DiffReporter struct {
path cmp.Path
diffs []string
}
func (r *DiffReporter) PushStep(ps cmp.PathStep) {
r.path = append(r.path, ps)
}
func (r *DiffReporter) Report(rs cmp.Result) {
if !rs.Equal() {
vx, vy := r.path.Last().Values()
r.diffs = append(r.diffs, fmt.Sprintf("%#v:\n\t-: %+v\n\t+: %+v\n", r.path, vx, vy))
}
}
func (r *DiffReporter) PopStep() {
r.path = r.path[:len(r.path)-1]
}
func (r *DiffReporter) String() string {
return strings.Join(r.diffs, "\n")
}

18
parser.go Normal file
View file

@ -0,0 +1,18 @@
package editorconfig
// Parser interface is responsible for the parsing of the ini file and the
// globbing patterns.
type Parser interface {
// ParseIni takes one .editorconfig (ini format) filename and returns its
// Editorconfig definition.
ParseIni(filename string) (*Editorconfig, error)
// ParseIni takes one .editorconfig (ini format) filename and returns its
// Editorconfig definition. In case of non fatal warnings, they are in
// a joined errors and might be ignored in some cases.
ParseIniGraceful(filename string) (*Editorconfig, error, error)
// FnmatchCase takes a pattern, a filename, and tells wether the given filename
// matches the globbing pattern.
FnmatchCase(pattern string, filename string) (bool, error)
}

43
simple_parser.go Normal file
View file

@ -0,0 +1,43 @@
package editorconfig
import (
"fmt"
"os"
"gopkg.in/ini.v1"
)
// SimpleParser implements the Parser interface but without doing any caching.
type SimpleParser struct{}
// ParseInit calls go-ini's Load on the file.
func (parser *SimpleParser) ParseIni(filename string) (*Editorconfig, error) {
ec, warning, err := parser.ParseIniGraceful(filename)
if err != nil {
return nil, err
}
return ec, warning
}
// ParseIni calls go-ini's Load on the file and keep warnings in a separate error.
func (parser *SimpleParser) ParseIniGraceful(filename string) (*Editorconfig, error, error) {
fp, err := os.Open(filename)
if err != nil {
return nil, nil, err //nolint:wrapcheck
}
defer fp.Close()
iniFile, err := ini.Load(fp)
if err != nil {
return nil, nil, fmt.Errorf("cannot load %q: %w", filename, err)
}
return newEditorconfig(iniFile)
}
// FnmatchCase calls the module's FnmatchCase.
func (parser *SimpleParser) FnmatchCase(selector string, filename string) (bool, error) {
return FnmatchCase(selector, filename)
}

18
testdata/.editorconfig vendored Normal file
View file

@ -0,0 +1,18 @@
# http://editorconfig.org/
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
indent_size = 8
[*.go]
indent_style = tab
indent_size = 4
[*.{js,css,less,htm,html}]
indent_style = space
indent_size = 2

22
testdata/a.ini vendored Normal file
View file

@ -0,0 +1,22 @@
# http://editorconfig.org/
root = true
[*]
end_of_line = lf
insert_final_newline = false
charset = utf-8
trim_trailing_whitespace = false
[*.go]
indent_style = tab
indent_size = 4
[*.{js,css,less,htm,html}]
indent_style = space
indent_size = 2
[invalid]
insert_final_newline = off
trim_trailing_whitespace = off
tab_width = off

5
testdata/root/.editorconfig vendored Normal file
View file

@ -0,0 +1,5 @@
root = true
[*.go]
indent_style = space
insert_final_newline = true

3
testdata/root/src/.editorconfig vendored Normal file
View file

@ -0,0 +1,3 @@
[*.go]
indent_size = 4
indent_style = tab

3
testdata/root/src/a.ini vendored Normal file
View file

@ -0,0 +1,3 @@
[*.go]
indent_size = 5
indent_style = space

7
testdata/root/src/dummy.go vendored Normal file
View file

@ -0,0 +1,7 @@
package main
import "fmt"
func main() {
fmt.Println("Dummy file")
}