diff --git a/jvm/CHANGELOG.md b/jvm/CHANGELOG.md index 70567d01..20b6d610 100644 --- a/jvm/CHANGELOG.md +++ b/jvm/CHANGELOG.md @@ -11,6 +11,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixed +- A single leading space (such as in the copyright header) should not override an otherwise 100% tab-indented file. ([#506](https://github.com/diffplug/selfie/issues/506)) ## [2.4.1] - 2024-10-07 ### Fixed diff --git a/jvm/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/EscapeLeadingWhitespace.kt b/jvm/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/EscapeLeadingWhitespace.kt index 71d1b281..04f154d3 100644 --- a/jvm/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/EscapeLeadingWhitespace.kt +++ b/jvm/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/EscapeLeadingWhitespace.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 DiffPlug + * Copyright (C) 2024-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ internal enum class EscapeLeadingWhitespace { .lineSequence() .mapNotNull { line -> val whitespace = line.takeWhile { it.isWhitespace() } - if (whitespace.isEmpty()) null + if (whitespace.isEmpty() || whitespace == " ") null else if (whitespace.all { it == ' ' }) ' ' else if (whitespace.all { it == '\t' }) '\t' else MIXED } diff --git a/jvm/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/EscapeLeadingWhitespaceTest.kt b/jvm/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/EscapeLeadingWhitespaceTest.kt index 6f130fd1..36943703 100644 --- a/jvm/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/EscapeLeadingWhitespaceTest.kt +++ b/jvm/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/EscapeLeadingWhitespaceTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 DiffPlug + * Copyright (C) 2024-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,9 +29,9 @@ class EscapeLeadingWhitespaceTest { appropriateFor("abc\nabc") shouldBe ALWAYS // all spaces -> only tabs need escape - appropriateFor(" ") shouldBe ONLY_ON_TAB + appropriateFor(" ") shouldBe ALWAYS appropriateFor(" ") shouldBe ONLY_ON_TAB - appropriateFor(" \n ") shouldBe ONLY_ON_TAB + appropriateFor(" \n ") shouldBe ONLY_ON_TAB // all tabs -> only space needs escape appropriateFor("\t") shouldBe ONLY_ON_SPACE @@ -39,6 +39,18 @@ class EscapeLeadingWhitespaceTest { appropriateFor("\t\n\t") shouldBe ONLY_ON_SPACE // it's a mess -> everything needs escape - appropriateFor("\t\n ") shouldBe ALWAYS + appropriateFor("\t\n ") shouldBe ALWAYS + + // single spaces and tabs -> only tabs need escape + appropriateFor( + """ +/* +${' '}* Copyright +${' '}*/ +interface Foo { +${'\t'}fun bar() +} +""") shouldBe + ONLY_ON_SPACE } } diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md index 46e4c391..4e489852 100644 --- a/python/CHANGELOG.md +++ b/python/CHANGELOG.md @@ -2,18 +2,16 @@ Changelog for the selfie Python libraries. -- [`com.diffplug.selfie:selfie-lib:VERSION`](https://pypi.org/project/selfie-lib/) -- [`com.diffplug.selfie:selfie-runner-pytest:VERSION`](https://pypi.org/project/pytest-selfie/) - The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +- [`selfie-lib:VERSION`](https://pypi.org/project/selfie-lib/) +- [`pytest-selfie:VERSION`](https://pypi.org/project/pytest-selfie/) + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +Allowable headings are `Added`, `Fixed`, and `Changed`. ## [Unreleased] -### Added -- TODO ### Fixed -- TODO -### Changed -- TODO +- A single leading space (such as in the copyright header) should not override an otherwise 100% tab-indented file. ([#506](https://github.com/diffplug/selfie/issues/506)) ## [1.0.0] - 2024-12-16 diff --git a/python/selfie-lib/selfie_lib/EscapeLeadingWhitespace.py b/python/selfie-lib/selfie_lib/EscapeLeadingWhitespace.py index b2e9695d..840d38f6 100644 --- a/python/selfie-lib/selfie_lib/EscapeLeadingWhitespace.py +++ b/python/selfie-lib/selfie_lib/EscapeLeadingWhitespace.py @@ -33,8 +33,8 @@ def appropriate_for(cls, file_content: str) -> "EscapeLeadingWhitespace": common_whitespace = None for line in file_content.splitlines(): - whitespace = "".join(c for c in line if c.isspace()) - if not whitespace: + whitespace = line[0 : len(line) - len(line.lstrip())] + if whitespace == "" or whitespace == " ": continue elif all(c == " " for c in whitespace): whitespace = " " diff --git a/python/selfie-lib/selfie_lib/__init__.py b/python/selfie-lib/selfie_lib/__init__.py index b7dea382..1c531129 100644 --- a/python/selfie-lib/selfie_lib/__init__.py +++ b/python/selfie-lib/selfie_lib/__init__.py @@ -6,6 +6,7 @@ from .CacheSelfie import cache_selfie_binary as cache_selfie_binary from .CacheSelfie import cache_selfie_json as cache_selfie_json from .CommentTracker import CommentTracker as CommentTracker +from .EscapeLeadingWhitespace import EscapeLeadingWhitespace as EscapeLeadingWhitespace from .FS import FS as FS from .Lens import Camera as Camera from .Lens import CompoundLens as CompoundLens diff --git a/python/selfie-lib/tests/EscapeLeadingWhitespace_test.py b/python/selfie-lib/tests/EscapeLeadingWhitespace_test.py new file mode 100644 index 00000000..d52b611c --- /dev/null +++ b/python/selfie-lib/tests/EscapeLeadingWhitespace_test.py @@ -0,0 +1,59 @@ +from selfie_lib import EscapeLeadingWhitespace + + +def test_detection(): + # not enough to detect + assert EscapeLeadingWhitespace.appropriate_for("") == EscapeLeadingWhitespace.ALWAYS + assert ( + EscapeLeadingWhitespace.appropriate_for("abc") == EscapeLeadingWhitespace.ALWAYS + ) + assert ( + EscapeLeadingWhitespace.appropriate_for("abc\nabc") + == EscapeLeadingWhitespace.ALWAYS + ) + + # all spaces -> only tabs need escape + assert ( + EscapeLeadingWhitespace.appropriate_for(" ") == EscapeLeadingWhitespace.ALWAYS + ) + assert ( + EscapeLeadingWhitespace.appropriate_for(" ") + == EscapeLeadingWhitespace.ONLY_ON_TAB + ) + assert ( + EscapeLeadingWhitespace.appropriate_for(" \n ") + == EscapeLeadingWhitespace.ONLY_ON_TAB + ) + + # all tabs -> only space needs escape + assert ( + EscapeLeadingWhitespace.appropriate_for("\t") + == EscapeLeadingWhitespace.ONLY_ON_SPACE + ) + assert ( + EscapeLeadingWhitespace.appropriate_for("\t\t") + == EscapeLeadingWhitespace.ONLY_ON_SPACE + ) + assert ( + EscapeLeadingWhitespace.appropriate_for("\t\n\t") + == EscapeLeadingWhitespace.ONLY_ON_SPACE + ) + + # it's a mess -> everything needs escape + assert ( + EscapeLeadingWhitespace.appropriate_for("\t\n ") + == EscapeLeadingWhitespace.ALWAYS + ) + + # single spaces and tabs -> only tabs need escape + tab = "\t" + test_string = f"""/* + * Copyright + */ +interface Foo [ +{tab}bar() +]""" + assert ( + EscapeLeadingWhitespace.appropriate_for(test_string) + == EscapeLeadingWhitespace.ONLY_ON_SPACE + ) diff --git a/python/selfie-lib/uv.lock b/python/selfie-lib/uv.lock index afec8460..84c8762a 100644 --- a/python/selfie-lib/uv.lock +++ b/python/selfie-lib/uv.lock @@ -112,7 +112,7 @@ wheels = [ [[package]] name = "selfie-lib" -version = "1.0.0" +version = "1.0.1.dev0" source = { virtual = "." } [package.dev-dependencies]