Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
595 changes: 0 additions & 595 deletions COPYING.md

This file was deleted.

File renamed without changes.
125 changes: 123 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,124 @@
# 1c-syntax utils
# utils

Common utils for 1c-syntax team java projects
[![Java CI](https://github.com/1c-syntax/utils/actions/workflows/check.yml/badge.svg)](https://github.com/1c-syntax/utils/actions/workflows/check.yml)
[![Maven Central](https://img.shields.io/maven-central/v/io.github.1c-syntax/utils.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/io.github.1c-syntax/utils)
[![GitHub release](https://img.shields.io/github/v/release/1c-syntax/utils?include_prereleases&sort=semver)](https://github.com/1c-syntax/utils/releases)
[![Java](https://img.shields.io/badge/Java-21%2B-orange.svg)](https://adoptium.net/)
[![License: LGPL-3.0-or-later](https://img.shields.io/badge/license-LGPL--3.0--or--later-blue.svg)](LICENSE.md)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | sed -n '1,120p'

echo "== README license mentions =="
if [ -f README.md ]; then
  nl -ba README.
# ... wait for user result? 
# Actually the commentary needs include capability only; I mistakenly added no output? done.

Repository: 1c-syntax/utils

Length of output: 1830


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files: README/build/gradle =="
git ls-files | rg '(^|/)(README\.md|build\.gradle\.kts|LICENSE.*|package\.json|pom\.xml)$'

echo "== README license mentions =="
if [ -f README.md ]; then
  rg -n -i 'LGPL|GNU|license|licence|spdx|lgpl' README.md -C 2
fi

echo "== Gradle license/pom mentions =="
rg -n -i 'LGPL|GNU|license|licence|spdx|licenseUrl|pom' build.gradle.kts -C 3 || true

echo "== source header/license mentions =="
rg -n -i 'LGPL-3\.0|LGPL|GNU LGPL|SPDX-License-Identifier|GNU Lesser General Public License' -g '!build*' -g '!build' -C 1 -M 8 || true

Repository: 1c-syntax/utils

Length of output: 2397


Синхронизируйте вариант лицензии в README и Maven-метаданных.

Раздел лицензии и бейдж README заявляют LGPL-3.0-or-later, а build.gradle.kts:135-147 публикует в POM GNU LGPL 3 с URL фиксированной LGPL 3.0. Выберите намеренный вариант и обновите соответствующие README, POM/заголовки исходников, чтобы публикация не противоречила опубликованному лицензированию.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 7, Align the README license badge and license section with
the Maven POM license metadata and source-file headers, choosing one intentional
LGPL variant and applying it consistently. Update the Gradle publishing
configuration that generates the POM, including the license name and URL, so the
published metadata matches the selected README and header wording.


Общие утилиты для java-проектов команды [1c-syntax](https://github.com/1c-syntax) —
небольшие независимые помощники, переиспользуемые в
[BSL Language Server](https://github.com/1c-syntax/bsl-language-server) и смежных проектах.

Библиотека сознательно держит минимальную замкнутость зависимостей и помечена
[JSpecify](https://jspecify.dev/) `@NullMarked` (типы считаются non-null, если не аннотированы
`@Nullable`).

## Требования

- Java 21 или новее (сборка таргетится на Java 21; CI прогоняется на Java 21 и 25).

## Подключение

Артефакт публикуется в Maven Central под координатами `io.github.1c-syntax:utils`.

### Gradle (Kotlin DSL)

```kotlin
dependencies {
implementation("io.github.1c-syntax:utils:VERSION")
}
```

### Maven

```xml
<dependency>
<groupId>io.github.1c-syntax</groupId>
<artifactId>utils</artifactId>
<version>VERSION</version>
</dependency>
```

Актуальную версию смотрите в
[релизах](https://github.com/1c-syntax/utils/releases) или на
[Maven Central](https://central.sonatype.com/artifact/io.github.1c-syntax/utils).

## Что внутри

### Пакет `com.github._1c_syntax.utils`

| Класс | Назначение |
| --- | --- |
| `Absolute` | Приведение файловых путей и URI к каноническому абсолютному виду — стабильный ключ для одного и того же файла независимо от формы записи. |
| `Lazy<T>` | Потокобезопасное хранилище значения с ленивым однократным вычислением (double-checked locking) и возможностью сброса кэша. |
| `GenericInterner<T>` | Потокобезопасный интернер значений: один канонический экземпляр на класс эквивалентности — экономия памяти и сравнение по ссылке. |
| `StringInterner` | Интернер строк на базе `GenericInterner`, дополнительно нормализующий `null` в пустую строку. |
| `CaseInsensitivePattern` | Компиляция регулярных выражений без учёта регистра с поддержкой Unicode (важно для кириллицы кода 1С). |

### Пакет `com.github._1c_syntax.utils.downloader`

Загрузчик исполняемого файла [BSL Language Server](https://github.com/1c-syntax/bsl-language-server)
из GitHub-релизов (для встраивания в IDE-плагины и другие клиенты).

| Класс | Назначение |
| --- | --- |
| `BslLanguageServerDownloader` | Скачивает подходящий под ОС ассет последнего релиза, распаковывает его и отдаёт путь к бинарю; кэширует установленную версию и опрашивает GitHub не чаще заданного интервала. |
| `GitHubReleaseClient` | Находит последний релиз выбранного канала через GitHub REST API (на `java.net.http.HttpClient` + gson, без клиентских библиотек GitHub). |
| `BslLanguageServerReleaseChannel` | Канал релизов: `STABLE` или `PRERELEASE`. |
| `DownloadProgressListener` | Слушатель прогресса скачивания ассета. |

## Примеры использования

Каноникализация пути и URI:

```java
Path path = Absolute.path("./src/../build/out.txt"); // абсолютный канонический путь
URI uri = Absolute.uri("file:///C:/Program%20Files/app"); // нормализованный file:-URI
```

Ленивое вычисление:

```java
Lazy<List<String>> lines = new Lazy<>(() -> readAllLines(file));
List<String> value = lines.getOrCompute(); // вычислится один раз и закэшируется
lines.clear(); // сбросить кэш
```

Интернирование:

```java
StringInterner interner = new StringInterner();
String canonical = interner.intern(name); // равные строки делят один экземпляр
```

Регистронезависимый поиск с учётом Unicode:

```java
Pattern pattern = CaseInsensitivePattern.compile("Процедура");
boolean matches = pattern.matcher("процедура").matches(); // true
```

Скачивание BSL Language Server:

```java
var client = new GitHubReleaseClient(githubToken); // токен может быть null
var downloader = new BslLanguageServerDownloader(installDir, client, HttpClient.newHttpClient());
Path binary = downloader.downloadIfNeeded(BslLanguageServerReleaseChannel.STABLE);
```

## Сборка

Используйте wrapper Gradle:

```bash
./gradlew build # сборка, проверки и тесты
./gradlew check # то, что гоняет CI (test + jacoco + javadoc + проверка лицензий)
```

Заголовки лицензии проверяются плагином license; при необходимости их можно проставить командой
`./gradlew licenseFormat`.

## Лицензия

[GNU LGPL 3.0 или новее](LICENSE.md) (`SPDX-License-Identifier: LGPL-3.0-or-later`).
59 changes: 37 additions & 22 deletions src/main/java/com/github/_1c_syntax/utils/Absolute.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,33 @@
import java.nio.file.Path;

/**
* Методы получения абсолютного пути файла с учетом различных особенностей
* Приведение файловых путей и URI к каноническому абсолютному виду.
*
* <p>Утилита сглаживает различия в записи одного и того же файла, из-за которых он иначе выглядел
* бы как разные ресурсы: относительные пути разворачиваются в абсолютные, символические ссылки и
* сегменты {@code .}/{@code ..} схлопываются через каноникализацию файла, регистр и разделители
* приводятся к виду файловой системы. Для {@code file:}-URI дополнительно нормализуется
* процентное кодирование спецсимволов и восстанавливается authority, чтобы форма URI совпадала с
* той, что отдаёт JDK для канонического файла.
*
* <p>Благодаря этому URI/путь можно использовать как стабильный ключ (например, документа в
* рабочей области), не опасаясь, что тот же файл придёт в другой записи.
*/
@UtilityClass
public final class Absolute {

/**
* Получение URI из строки
* Разбирает строковый URI и приводит его к каноническому абсолютному виду.
*
* <p>Если строка не является корректным URL, она интерпретируется как {@link URI} и обрабатывается
* через {@link #uri(URI)}.
*
* @param uri - строковое представление URI
* @return - полученное значение
* @param uri строковое представление URI
* @return канонический абсолютный URI
Comment on lines +37 to +59

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
rg -n -C3 'Absolute\.(uri|path)\(' src/main/java src/test/java

Repository: 1c-syntax/utils

Length of output: 13624


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Map and inspect the Absolute utility implementation around doc and methods, and relevant tests.
ast-grep outline src/main/java/com/github/_1c_syntax/utils/Absolute.java || true
printf '\n--- Absolute.java relevant sections ---\n'
sed -n '1,180p' src/main/java/com/github/_1c_syntax/utils/Absolute.java | cat -n

printf '\n--- AbsoluteTest.java relevant URI behavior tests ---\n'
sed -n '1,230p' src/test/java/com/github/_1c_syntax/utils/AbsoluteTest.java | cat -n

Repository: 1c-syntax/utils

Length of output: 14168


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Probe JDK URI semantics and Absolute helper behavior using only repository source and runtime types
# without running repository project code.
python3 - <<'PY'
from pathlib import Path
p = Path("src/main/java/com/github/_1c_syntax/utils/Absolute.java")
text = p.read_text()
print("contains normalize() call:", "normalize()" in text or "URI.normalize" in text)
print("handles query/fragment explicitly:", any(name in text for name in ("getQuery", "getFragment", "getSchemeSpecificPart", "new URI(")))
PY

# Run focused Java probe in memory for JDK URI behavior and Path.of(URI)/File normalization as strings.
# No repository files are imported or executed; only JDK classes are used.
javac -d /tmp/jdk-uri-probe - <<'JAVA'
import java.net.*;
import java.io.*;

public class UriProbe {
  public static void main(String[] args) throws Exception {
    String[] inputs = {
      "file:///fake?query#frag",
      "file:///a/./b/../fake.txt",
      "untitled://server/x?query#frag"
    };
    for (String s : inputs) {
      URI u = new URI(s);
      String psp = u.getSchemeSpecificPart();
      String path = u.getPath();
      URI normalized = u.normalize();
      System.out.println("INPUT=" + s);
      System.out.println("  scheme=" + u.getScheme()
                       + " schemeSpecificPart=" + psp
                       + " path=" + path
                       + " query=" + u.getQuery()
                       + " fragment=" + u.getFragment()
                       + " normalizedPath=" + normalized.getPath());
    }
    File f = new File(new URI("file:///C%3A/a/../b?query#frag"));
    System.out.println("FILE_URI_TO_FILE=" + f.toString());
  }
}
JAVA

java -cp /tmp/jdk-uri-probe UriProbe

Repository: 1c-syntax/utils

Length of output: 329


Не описывайте uri(URI) как нормализацию произвольных URI.

Метод кодирует полный getSchemeSpecificPart() и не сохраняет query/fragment отдельно: git://x/path?query#frag превращается в path с закодированными ? и #, а ./.. не схлопываются. Подрядки обещают стабильный ключ только для файловых URI, поэтому сузьте Javadoc до поддерживаемых файловых случаев или поправьте реализацию и покройте edge cases тестами.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/github/_1c_syntax/utils/Absolute.java` around lines 37 -
59, Сузьте Javadoc класса Absolute и метода uri(URI), не заявляя каноникализацию
произвольных URI или сохранение query/fragment; явно ограничьте обещания
поддерживаемыми файловыми URI и стабильными ключами для них. Не изменяйте
реализацию или добавляйте тесты, поскольку комментарий допускает исправление
только документации.

*/
public static URI uri(String uri) {
try {
var url = new URL(uri.replace("+", "%2B").replace("%%", "%25%"));

Check warning on line 63 in src/main/java/com/github/_1c_syntax/utils/Absolute.java

View workflow job for this annotation

GitHub Actions / build (25, ubuntu-latest)

[deprecation] URL(String) in URL has been deprecated

Check warning on line 63 in src/main/java/com/github/_1c_syntax/utils/Absolute.java

View workflow job for this annotation

GitHub Actions / build (21, ubuntu-latest)

[deprecation] URL(String) in URL has been deprecated

Check warning on line 63 in src/main/java/com/github/_1c_syntax/utils/Absolute.java

View workflow job for this annotation

GitHub Actions / build (25, macOS-latest)

[deprecation] URL(String) in URL has been deprecated

Check warning on line 63 in src/main/java/com/github/_1c_syntax/utils/Absolute.java

View workflow job for this annotation

GitHub Actions / build (21, macOS-latest)

[deprecation] URL(String) in URL has been deprecated

Check warning on line 63 in src/main/java/com/github/_1c_syntax/utils/Absolute.java

View workflow job for this annotation

GitHub Actions / build (21, macOS-latest)

[deprecation] URL(String) in URL has been deprecated

Check warning on line 63 in src/main/java/com/github/_1c_syntax/utils/Absolute.java

View workflow job for this annotation

GitHub Actions / build (25, ubuntu-latest)

[deprecation] URL(String) in URL has been deprecated

Check warning on line 63 in src/main/java/com/github/_1c_syntax/utils/Absolute.java

View workflow job for this annotation

GitHub Actions / build (21, ubuntu-latest)

[deprecation] URL(String) in URL has been deprecated

Check warning on line 63 in src/main/java/com/github/_1c_syntax/utils/Absolute.java

View workflow job for this annotation

GitHub Actions / build (25, macOS-latest)

[deprecation] URL(String) in URL has been deprecated
var decodedPath = URLDecoder.decode(url.getPath(), StandardCharsets.UTF_8);
var decodedUri = new URI(
url.getProtocol(),
Expand All @@ -66,10 +79,11 @@
}

/**
* Получение абсолютного URI из URI с валидацией
* Приводит URI к каноническому абсолютному виду, нормализуя процентное кодирование пути и,
* для {@code file:}-URI без authority, восстанавливая её через каноникализацию файла.
*
* @param uri - исходный URI
* @return - полученное значение
* @param uri исходный URI
* @return канонический абсолютный URI
*/
public static URI uri(URI uri) {
var decodedUri = URI.create(uri.getScheme() + ":" + encodePath(uri.getSchemeSpecificPart()));
Expand All @@ -78,50 +92,51 @@
}

/**
* Получение URI файла
* Возвращает канонический абсолютный URI файла.
*
* @param file - исходный файл
* @return - полученное значение
* @param file исходный файл
* @return канонический абсолютный {@code file:}-URI
*/
public static URI uri(File file) {
return uri(path(file).toUri());
}

/**
* Получение пути (path) из строки
* Возвращает канонический абсолютный путь по его строковому представлению.
*
* @param path - строковое представление пути
* @return - полученное значение
* @param path строковое представление пути
* @return канонический абсолютный путь
*/
public static Path path(String path) {
return path(Path.of(path));
}

/**
* Получение пути (path) из URI
* Возвращает канонический абсолютный путь к файлу, на который указывает URI.
*
* @param uri - исходное значение URI
* @return - полученное значение
* @param uri исходный URI
* @return канонический абсолютный путь
*/
public static Path path(URI uri) {
return path(Path.of(uri(uri)));
Comment on lines 114 to 121

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
rg -n -C3 'path\(URI uri\)|Path\.of\(uri' src/main/java src/test/java

Repository: 1c-syntax/utils

Length of output: 794


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== Candidate files =="
fd -a 'Absolute\.java$' . | sed 's#^\./##'

echo "== Absolute.java relevant sections =="
sed -n '1,170p' src/main/java/com/github/_1c_syntax/utils/Absolute.java

echo "== References to Absolute.path =="
rg -n -C3 'Absolute\.path|\.path\(Path\.(of)?\(|path\(' src/main/java src/test/java | sed -n '1,200p'

echo "== Java Path.of URI documentation excerpt =="
python3 - <<'PY'
# Programmatic source-level check: show source string for Path.of(String) and Uri pattern requirements.
print("Javadoc/source context is not available from repository; rely on Java API contract.")
PY

Repository: 1c-syntax/utils

Length of output: 12087


🌐 Web query:

Java Path.of(URI) file URI scheme IllegalArgumentException javadoc

💡 Result:

The Path.of(URI) method in Java throws an IllegalArgumentException if the provided URI does not meet specific requirements associated with the available file system providers [1][2]. The method works by iterating over all installed FileSystemProviders to locate one that matches the URI's scheme [1][3]. If a suitable provider is found, it is then tasked with converting the URI into a Path [1][2]. For the default file system provider, which handles the "file" URI scheme, the following preconditions must be met [1][4]: 1. The URI must have a non-empty path component [1][5]. 2. The URI must have undefined query and fragment components [1][2]. If the URI scheme is not recognized (i.e., no installed provider supports it), or if the URI fails to meet the specific requirements of the chosen provider (such as the default provider's requirement for a non-empty path and absence of query/fragment components), an IllegalArgumentException is thrown [1][6][4]. Additionally, if a URI has no scheme at all, the method will fail because the scheme is required to identify the correct provider [7].

Citations:


Укажите схему file: в документации path(URI).

Path.of(URI) ожидает URI в файловом пространстве имён; URI другой схемы приведут к IllegalArgumentException. Добавьте это требование в @param, либо опишите как поддерживаемый случай в @throws.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/github/_1c_syntax/utils/Absolute.java` around lines 114 -
121, Обновите документацию метода path(URI), явно указав, что параметр uri
должен использовать схему file:. Зафиксируйте ожидаемое поведение для URI другой
схемы через `@throws` IllegalArgumentException, не изменяя реализацию метода.

}

/**
* Получение абсолютного пути (path) из Path
* Возвращает канонический абсолютный путь для переданного {@link Path}.
*
* @param path - исходное значение пути
* @return - полученное значение
* @param path исходный путь
* @return канонический абсолютный путь
*/
public static Path path(Path path) {
return path(path.toFile());
}

/**
* Получение пути файла
* Возвращает канонический абсолютный путь файла: разворачивает символические ссылки и
* сегменты {@code .}/{@code ..}, приводит запись к виду файловой системы.
*
* @param file - исходный файл
* @return - полученное значение
* @param file исходный файл
* @return канонический абсолютный путь
*/
@SneakyThrows
public static Path path(File file) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@
import java.util.regex.PatternSyntaxException;

/**
* Pattern helper
* Компиляция регулярных выражений, нечувствительных к регистру, включая Unicode.
*
* <p>Обёртка над {@link Pattern#compile(String, int)} с флагами
* {@link Pattern#CASE_INSENSITIVE} и {@link Pattern#UNICODE_CASE}. Второй флаг обязателен для
* корректного сравнения без учёта регистра в неlatin-алфавитах (в частности, в кириллице —
* основном алфавите кода 1С), где одного {@code CASE_INSENSITIVE} недостаточно.
*/
@UtilityClass
public class CaseInsensitivePattern {
Expand Down
28 changes: 24 additions & 4 deletions src/main/java/com/github/_1c_syntax/utils/GenericInterner.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,37 @@
import java.util.concurrent.ConcurrentHashMap;

/**
* Реализация универсального интернера
* Потокобезопасный интернер значений произвольного типа.
*
* <p>Хранит по одному каноническому экземпляру для каждого класса эквивалентности
* (по {@code equals}/{@code hashCode}) и возвращает его для всех равных значений. Позволяет
* заменить множество равных, но разных по ссылке объектов на один и тем самым сократить
* потребление памяти, а для потребителей — сравнивать значения по ссылке ({@code ==}).
*
* <p>Кэш не имеет ограничения по размеру и не вытесняет записи автоматически: интернированные
* значения удерживаются до явного {@link #clear()}. Реализация основана на
* {@link ConcurrentHashMap} и безопасна для конкурентного использования.
*
* @param <T> тип интернируемых значений
*/
public class GenericInterner<T> {

private final Map<T, T> map = new ConcurrentHashMap<>();

/**
* Метод интернирования значения
* Создаёт пустой интернер.
*/
public GenericInterner() {
// no state to initialize beyond the backing map
}

/**
* Возвращает канонический экземпляр, равный переданному значению. Если равное значение ещё не
* интернировано, каноническим становится переданный объект.
*
* @param object Интернируемый объект
* @return значение из кеша
* @param object интернируемое значение
* @return ранее сохранённый экземпляр, равный {@code object}, либо сам {@code object},
* если равного ещё не было
*/
public T intern(T object) {
T exist = map.putIfAbsent(object, object);
Expand Down
Loading
Loading