From 23691966d8bdb1f6ef947afefa970b896efb393f Mon Sep 17 00:00:00 2001 From: "Ekaterina.Itsenko" Date: Wed, 29 Oct 2025 22:45:42 +0000 Subject: [PATCH] [pycharm] PY-84775 Update UI text inspection + add tests [pycharm] PY-84775 Fix issues with CapitalizationInspectionTest [pycharm] PY-84775 Add one more assertion to check capitalization [pycharm] PY-84775 Update UI text inspection + add tests Merge-request: IJ-MR-179263 Merged-by: Ekaterina Itsenko GitOrigin-RevId: 289bbc55860e3f4f009b8a4c5c1fbf635e712bf5 --- .../codeInspection/NlsCapitalizationUtil.java | 168 +++++++++++++++++- .../NlsCapitalizationUtilTest.java | 73 ++++++++ 2 files changed, 235 insertions(+), 6 deletions(-) diff --git a/jvm/jvm-analysis-api/src/com/intellij/codeInspection/NlsCapitalizationUtil.java b/jvm/jvm-analysis-api/src/com/intellij/codeInspection/NlsCapitalizationUtil.java index 44714c2dad94..29309c2dbe35 100644 --- a/jvm/jvm-analysis-api/src/com/intellij/codeInspection/NlsCapitalizationUtil.java +++ b/jvm/jvm-analysis-api/src/com/intellij/codeInspection/NlsCapitalizationUtil.java @@ -5,25 +5,92 @@ import com.intellij.openapi.util.text.StringUtil; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; -import java.util.List; +import java.util.*; +import java.util.regex.Pattern; public final class NlsCapitalizationUtil { + private static final Set TITLE_CASE_LOWERCASE_WORDS = Set.of( + "a", "an", "the", + "and", "or", "but", + "at", "by", "for", "from", "in", "into", "of", "off", "on", "onto", "out", "over", "to", "up", "with" + ); + private static final Pattern PERIOD_PATTERN = Pattern.compile("\\.(?!\\s*$)"); + private static final Pattern DOUBLE_QUOTES_PATTERN = Pattern.compile("[“”\"]"); + private static final Pattern EXCLAMATION_PATTERN = Pattern.compile("!"); + private static final Pattern CONTRACTION_PATTERN = Pattern.compile("(?i)\\b(can't|won't|isn't|aren't|wasn't|weren't|hasn't|haven't|hadn't|doesn't|don't|didn't|shouldn't|wouldn't|couldn't|mightn't|mustn't)\\b(? splitByWhitespace(String s) { + return Arrays.stream(s.trim().split("\\s+")) + .filter(str -> !str.isEmpty()) + .toList(); + } + + private static boolean checkTitleCapitalization(@NotNull String value) { + List words = splitByWhitespace(value); + final int wordCount = words.size(); + if (wordCount == 0) return true; + for (int i = 0; i < wordCount; i++) { + String word = words.get(i); + if (word.isEmpty()) continue; + String cleanWord = stripPunctuation(word); + if (cleanWord.isEmpty()) continue; + // Check if it's a special case (like iOS, macOS) + if (hasInternalCapitalization(cleanWord)) { + continue; + } + if (i == 0 || i == wordCount - 1) { + if (!isCapitalizedWord(cleanWord)) return false; + } + else { + String lowerWord = cleanWord.toLowerCase(Locale.ENGLISH); + if (TITLE_CASE_LOWERCASE_WORDS.contains(lowerWord)) { + if (isCapitalizedWord(cleanWord)) return false; + } + else { + if (!isCapitalizedWord(cleanWord)) return false; + } + } + } + return true; + } + + private static boolean hasInternalCapitalization(@NotNull String word) { + if (word.length() <= 1) return false; + boolean hasLowerCase = false; + boolean hasUpperCaseAfterFirst = false; + for (int i = 0; i < word.length(); i++) { + char c = word.charAt(i); + if (Character.isLetter(c)) { + if (Character.isLowerCase(c)) { + hasLowerCase = true; + } + else if (i > 0 && Character.isUpperCase(c)) { + hasUpperCaseAfterFirst = true; + } + } + } + return hasLowerCase && hasUpperCaseAfterFirst; + } + private static boolean checkSentenceCapitalization(@NotNull String value) { List words = StringUtil.split(value, " "); final int wordCount = words.size(); if (wordCount == 0) return true; - if (Character.isLetter(words.get(0).charAt(0)) && !isCapitalizedWord(words.get(0))) return false; + if (Character.isLetter(words.getFirst().charAt(0)) && !isCapitalizedWord(words.getFirst())) return false; if (wordCount == 1) return true; int capitalized = 1; @@ -49,9 +116,98 @@ public final class NlsCapitalizationUtil { return !word.isEmpty() && Character.isLetter(word.charAt(0)) && Character.isUpperCase(word.charAt(0)); } + private static @NotNull String stripPunctuation(@NotNull String word) { + int start = 0; + int end = word.length(); + while (start < end && !Character.isLetterOrDigit(word.charAt(start))) { + start++; + } + while (end > start && !Character.isLetterOrDigit(word.charAt(end - 1))) { + end--; + } + return start < end ? word.substring(start, end) : ""; + } + + public static boolean checkPunctuation(@NotNull String value) { + if (PERIOD_PATTERN.matcher(value).find()) { + return value.endsWith("."); + } + if (value.endsWith(".")) { + return false; + } + if (DOUBLE_QUOTES_PATTERN.matcher(value).find()) { + return false; + } + if (EXCLAMATION_PATTERN.matcher(value).find()) { + return false; + } + if (CONTRACTION_PATTERN.matcher(value).find()) { + return false; + } + return true; + } + public static @NotNull String fixValue(String string, Nls.Capitalization capitalization) { - return capitalization == Nls.Capitalization.Title - ? StringUtil.wordsToBeginFromUpperCase(string) - : StringUtil.capitalize(StringUtil.wordsToBeginFromLowerCase(string)); + if (capitalization == Nls.Capitalization.Title) { + return fixTitleCapitalization(string); + } + else { + return StringUtil.capitalize(StringUtil.wordsToBeginFromLowerCase(string)); + } + } + + private static String fixTitleCapitalization(String text) { + if (text == null || text.isBlank()) return text; + String[] tokens = WHITESPACE_SPLIT_PATTERN.split(text); + + int firstWordIndex = -1, lastWordIndex = -1; + for (int i = 0; i < tokens.length; i++) { + String cleanedToken = stripPunctuation(tokens[i]); + if (!cleanedToken.isEmpty()) { + if (firstWordIndex == -1) firstWordIndex = i; + lastWordIndex = i; + } + } + if (firstWordIndex == -1) return text; + + StringBuilder result = new StringBuilder(); + for (int i = 0; i < tokens.length; i++) { + String token = tokens[i]; + if (token.isBlank()) { + result.append(token); + continue; + } + String cleanedToken = stripPunctuation(token); + if (cleanedToken.isEmpty()) { + result.append(token); + continue; + } + String lowercaseToken = cleanedToken.toLowerCase(Locale.ENGLISH); + boolean isFirstWord = i == firstWordIndex; + boolean isLastWord = i == lastWordIndex; + + if (!isFirstWord && !isLastWord && TITLE_CASE_LOWERCASE_WORDS.contains(lowercaseToken)) { + result.append(applyLowercaseToTokenPreservingPunctuation(token, lowercaseToken)); + } else if (hasInternalCapitalization(cleanedToken)) { + result.append(token); + } else { + result.append(capitalizeFirstLetter(token)); + } + } + return result.toString(); + } + + private static String applyLowercaseToTokenPreservingPunctuation(String token, String lowercaseWord) { + var matcher = PUNCTUATION_WITH_WORD_PATTERN.matcher(token); + return matcher.matches() ? matcher.group(1) + lowercaseWord + matcher.group(3) : token; + } + + private static String capitalizeFirstLetter(String token) { + if (token.isEmpty() || SPECIAL_PREFIX_PATTERN.matcher(token).matches()) return token; + var matcher = LEADING_PUNCTUATION_WITH_FIRST_LETTER_PATTERN.matcher(token); + if (matcher.matches()) { + return matcher.group(1) + Character.toUpperCase(matcher.group(2).charAt(0)) + matcher.group(3); + } + return token; } } diff --git a/jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/NlsCapitalizationUtilTest.java b/jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/NlsCapitalizationUtilTest.java index 1d5eca0ed7c5..32d73e624288 100644 --- a/jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/NlsCapitalizationUtilTest.java +++ b/jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/NlsCapitalizationUtilTest.java @@ -18,6 +18,7 @@ package com.intellij.codeInspection; import junit.framework.TestCase; import org.jetbrains.annotations.Nls; +import static com.intellij.codeInspection.NlsCapitalizationUtil.checkPunctuation; import static com.intellij.codeInspection.NlsCapitalizationUtil.isCapitalizationSatisfied; public class NlsCapitalizationUtilTest extends TestCase { @@ -99,4 +100,76 @@ public class NlsCapitalizationUtilTest extends TestCase { private static void assertNotCapitalization(String value, Nls.Capitalization capitalization) { assertFalse("'" + value + "' should not satisfy " + capitalization, isCapitalizationSatisfied(value, capitalization)); } + + public void testTitleCapitalizationArticles() { + assertTitle("Compare with the Latest Repository Version"); + assertTitle("A Good Example"); + assertTitle("An Important Message"); + } + + public void testTitleCapitalizationConjunctions() { + assertTitle("Search and Replace"); + assertTitle("Save or Discard Changes"); + assertTitle("This but Not That"); + } + + public void testTitleCapitalizationPrepositions() { + assertTitle("Compare with Latest Version"); + assertTitle("Search in Files"); + assertTitle("Go to Declaration"); + assertTitle("Copy from Here"); + } + + public void testTitleCapitalizationFirstAndLastWords() { + assertTitle("Compare With"); + assertTitle("The First Word"); + assertTitle("Go to the End"); + } + + public void testTitleCapitalizationNotSatisfied() { + assertNotCapitalization("Compare With The Latest Repository Version", Nls.Capitalization.Title); + assertNotCapitalization("Search And Replace", Nls.Capitalization.Title); + assertNotCapitalization("compare with Latest Version", Nls.Capitalization.Title); + assertNotCapitalization("Compare with", Nls.Capitalization.Title); + } + + public void testPunctuationSingleSentenceNoPeriod() { + assertTrue("Single sentence should not have period", checkPunctuation("This is a single sentence")); + } + + public void testPunctuationSingleSentenceWithPeriod() { + assertFalse("Single sentence should not end with period", checkPunctuation("This is a single sentence.")); + } + + public void testPunctuationMultipleSentencesWithPeriods() { + assertTrue("Multiple sentences should end with periods", checkPunctuation("First sentence. Second sentence.")); + } + + public void testPunctuationMultipleSentencesWithoutFinalPeriod() { + assertFalse("Multiple sentences should end with periods", checkPunctuation("First sentence. Second sentence")); + } + + public void testPunctuationNoDoubleQuotes() { + assertFalse("Should not use double quotes", checkPunctuation("This is \"quoted\" text")); + } + + public void testPunctuationSingleQuotesAllowed() { + assertTrue("Single quotes are allowed", checkPunctuation("This is 'quoted' text")); + } + + public void testPunctuationNoExclamation() { + assertFalse("Should not use exclamation points", checkPunctuation("This is important!")); + } + + public void testPunctuationNoContractions() { + assertFalse("Should not use contractions", checkPunctuation("Path can't be found")); + } + + public void testPunctuationDontAgainAllowed() { + assertTrue("'Don't [verb] again' is allowed", checkPunctuation("Don't ask again")); + } + + public void testFixValueTitleWithArticles() { + assertEquals("Compare with the Latest Version", NlsCapitalizationUtil.fixValue("Compare With The Latest Version", Nls.Capitalization.Title)); + } } \ No newline at end of file