Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #3972: Added TextViewStyleCheck script #5599

Open
wants to merge 39 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
e243e20
adding text view style check script
manas-yu Dec 16, 2024
00f7367
formatting
manas-yu Dec 16, 2024
25e31c0
error print fix
manas-yu Dec 16, 2024
0e7240c
Merge branch 'develop' into text-view-script
manas-yu Dec 17, 2024
8afffd9
Merge branch 'develop' into text-view-script
manas-yu Dec 20, 2024
0cda3ff
Merge branch 'develop' into text-view-script
manas-yu Dec 23, 2024
ecb097e
Merge branch 'develop' into text-view-script
manas-yu Dec 27, 2024
b3247dd
Merge branch 'develop' into text-view-script
manas-yu Dec 29, 2024
284759b
Merge branch 'develop' into text-view-script
manas-yu Dec 30, 2024
50155ab
edge cases
manas-yu Jan 8, 2025
781d682
formatting
manas-yu Jan 8, 2025
d66f9aa
Merge branch 'text-view-script' of https://github.com/manas-yu/oppia-…
manas-yu Jan 8, 2025
8b1f90f
class fix
manas-yu Jan 8, 2025
b5155bd
removing redundant checks
manas-yu Jan 8, 2025
0631ec7
checking style
manas-yu Jan 8, 2025
f104b96
kdoc fix
manas-yu Jan 8, 2025
8bd90bf
logging line no. and CI integration
manas-yu Jan 10, 2025
9e530aa
formatting
manas-yu Jan 10, 2025
051ab79
Merge branch 'develop' into text-view-script
manas-yu Jan 10, 2025
0510d7d
test file
manas-yu Jan 10, 2025
f24b7f6
Merge branch 'text-view-script' of https://github.com/manas-yu/oppia-…
manas-yu Jan 10, 2025
ec4c12d
bazel setup
manas-yu Jan 10, 2025
67ab290
revert
manas-yu Jan 10, 2025
afcb708
avoid redundant RTL/LTR settings checks
manas-yu Jan 10, 2025
b5d50a8
Merge branch 'develop' into text-view-script
manas-yu Jan 15, 2025
6caa1a9
Merge branch 'oppia:develop' into text-view-script
manas-yu Jan 15, 2025
992e644
revert
manas-yu Jan 15, 2025
73c90aa
adding ids
manas-yu Jan 15, 2025
7670404
updating tests
manas-yu Jan 15, 2025
5f7c85d
formatting
manas-yu Jan 15, 2025
ca14146
adding style
manas-yu Jan 16, 2025
8602e7e
kdoc and todo exemp
manas-yu Jan 16, 2025
e8ad25c
line number
manas-yu Jan 17, 2025
2fb3eff
formatting
manas-yu Jan 17, 2025
6e76303
attri list
manas-yu Jan 17, 2025
c80cfe9
kdoc
manas-yu Jan 17, 2025
4bdbe27
Deque and line no
manas-yu Jan 19, 2025
72e30eb
formatting
manas-yu Jan 19, 2025
1e0a43f
Merge branch 'develop' into text-view-script
manas-yu Jan 21, 2025
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
7 changes: 7 additions & 0 deletions scripts/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -312,3 +312,10 @@ java_binary(
main_class = "org.oppia.android.scripts.telemetry.DecodeUserStudyEventStringKt",
runtime_deps = ["//scripts/src/java/org/oppia/android/scripts/telemetry:decode_user_study_event_string_lib"],
)

kt_jvm_binary(
name = "check_textview_styles",
testonly = True,
main_class = "org.oppia.android.scripts.xml.TextViewStyleCheckKt",
runtime_deps = ["//scripts/src/java/org/oppia/android/scripts/xml:check_textview_styles"],
)
11 changes: 11 additions & 0 deletions scripts/src/java/org/oppia/android/scripts/xml/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,14 @@ kt_jvm_library(
"//scripts/src/java/org/oppia/android/scripts/common:repository_file",
],
)

kt_jvm_library(
name = "check_textview_styles",
testonly = True,
srcs = ["TextViewStyleCheck.kt"],
visibility = ["//scripts:oppia_script_binary_visibility"],
deps = [
":xml_syntax_error_handler",
"//scripts/src/java/org/oppia/android/scripts/common:repository_file",
],
)
173 changes: 173 additions & 0 deletions scripts/src/java/org/oppia/android/scripts/xml/TextViewStyleCheck.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package org.oppia.android.scripts.xml

import org.w3c.dom.Element
import java.io.File
import javax.xml.parsers.DocumentBuilderFactory

/**
* Script to ensure all TextView elements in layout XML files use centrally managed styles.
*
* Usage:
* bazel run //scripts:check_textview_styles -- <path_to_repository_root>
*
* Arguments:
* - path_to_repository_root: The root path of the repository.
*
* Example:
* bazel run //scripts:check_textview_styles -- $(pwd)
Rd4dev marked this conversation as resolved.
Show resolved Hide resolved
*/
fun main(vararg args: String) {
require(args.isNotEmpty()) {
"Usage: bazel run //scripts:check_textview_styles -- <path_to_repository_root>"
}

val repoRoot = File(args[0])
require(repoRoot.exists()) { "Repository root path does not exist: ${args[0]}" }

val resDir = File(repoRoot, "app/src/main/res")
require(resDir.exists()) { "Resource directory does not exist: ${resDir.path}" }

val xmlFiles = resDir.listFiles { file -> file.isDirectory && file.name.startsWith("layout") }
?.flatMap { dir -> dir.walkTopDown().filter { it.extension == "xml" } }
?: emptyList()

val styleChecker = TextViewStyleCheck(repoRoot)
styleChecker.checkFiles(xmlFiles)
}

private class TextViewStyleCheck(private val repoRoot: File) {
private val errors = mutableListOf<String>()
Rd4dev marked this conversation as resolved.
Show resolved Hide resolved
private val legacyDirectionalityWarnings = mutableListOf<String>()
private val builderFactory = DocumentBuilderFactory.newInstance()
private val styles: Map<String, Element> by lazy { loadStyles() }

private fun loadStyles(): Map<String, Element> {
val stylesFile = File(repoRoot, "app/src/main/res/values/styles.xml")
require(stylesFile.exists()) { "Styles file does not exist: ${stylesFile.path}" }

val document = builderFactory.newDocumentBuilder().parse(stylesFile)
val styleNodes = document.getElementsByTagName("style")
return (0 until styleNodes.length).associate { i ->
val element = styleNodes.item(i) as Element
element.getAttribute("name") to element
}
}
/** Checks XML files for TextView elements to ensure compliance with style requirements. */
fun checkFiles(xmlFiles: List<File>) {
xmlFiles.forEach { file -> processXmlFile(file) }
printResults()
}

private fun processXmlFile(file: File) {
val document = builderFactory.newDocumentBuilder().parse(file)
val textViewNodes = document.getElementsByTagName("TextView")

for (i in 0 until textViewNodes.length) {
val element = textViewNodes.item(i) as Element
validateTextViewElement(element, file.path)
}
}

private fun validateTextViewElement(element: Element, filePath: String) {
val styleAttribute = element.attributes.getNamedItem("style")?.nodeValue
val idAttribute = element.attributes.getNamedItem("android:id")?.nodeValue ?: "No ID"

if (!isExemptFromStyleRequirement(element)) {
if (styleAttribute?.startsWith("@style/") == true) {
validateStyle(styleAttribute, idAttribute, filePath)
} else {
errors.add("$filePath: TextView ($idAttribute) requires central style.")
Rd4dev marked this conversation as resolved.
Show resolved Hide resolved
}
}

checkForLegacyDirectionality(element, filePath)
}
// Validate if the referenced style exists and contains necessary RTL/LTR properties.
private fun validateStyle(styleAttribute: String, idAttribute: String, filePath: String) {
Rd4dev marked this conversation as resolved.
Show resolved Hide resolved
val styleName = styleAttribute.removePrefix("@style/")
val styleElement = styles[styleName] ?: run {
errors.add("$filePath: TextView ($idAttribute) references non-existent style: $styleName")
return
}

val items = styleElement.getElementsByTagName("item")
val hasRtlProperties = (0 until items.length).any { i ->
val item = items.item(i) as Element
when (item.getAttribute("name")) {
"android:textAlignment",
"android:gravity",
"android:layoutDirection",
"android:textDirection",
"android:textSize" -> true
else -> false
}
}

if (!hasRtlProperties) {
errors.add(
"$filePath: TextView ($idAttribute) style '$styleName' lacks RTL/LTR properties"
)
}
}
// Determines if a TextView is exempt from requiring a centrally managed style.
private fun isExemptFromStyleRequirement(element: Element): Boolean {
if (element.getAttribute("android:gravity")?.contains("center") == true) return true
Rd4dev marked this conversation as resolved.
Show resolved Hide resolved
if (hasDynamicVisibility(element)) return true
if (element.hasAttribute("android:textSize")) return true
Rd4dev marked this conversation as resolved.
Show resolved Hide resolved

return !hasDirectionalAttributes(element)
Rd4dev marked this conversation as resolved.
Show resolved Hide resolved
}

private fun hasDynamicVisibility(element: Element) =
element.getAttribute("android:visibility").let { it.contains("{") && it.contains("}") }

private fun hasDirectionalAttributes(element: Element): Boolean {
val directionAttributes = listOf(
"android:layout_alignParentStart",
"android:layout_alignParentEnd",
"android:layout_toStartOf",
"android:layout_toEndOf",
"android:paddingStart",
"android:paddingEnd",
"android:layout_marginStart",
"android:layout_marginEnd"
)
return directionAttributes.any { element.hasAttribute(it) }
}

private fun checkForLegacyDirectionality(element: Element, filePath: String) {
val legacyAttributes = listOf(
"android:paddingLeft",
"android:paddingRight",
"android:layout_marginLeft",
"android:layout_marginRight",
"android:layout_alignParentLeft",
"android:layout_alignParentRight",
"android:layout_toLeftOf",
"android:layout_toRightOf"
)

val foundLegacyAttributes = legacyAttributes.filter { element.hasAttribute(it) }
if (foundLegacyAttributes.isNotEmpty()) {
legacyDirectionalityWarnings.add(
"$filePath: TextView uses legacy" +
" directional attributes: ${foundLegacyAttributes.joinToString(", ")}"
)
}
}

private fun printResults() {
if (legacyDirectionalityWarnings.isNotEmpty()) {
println("\nWarnings - Legacy directionality attributes found:")
legacyDirectionalityWarnings.forEach { println(it) }
}

if (errors.isNotEmpty()) {
println("\nTextView Style Check FAILED:")
errors.forEach { println(it) }
throw Exception("Some TextView elements do not have centrally managed styles.")
Rd4dev marked this conversation as resolved.
Show resolved Hide resolved
} else {
println("\nTextView Style Check PASSED.")
}
}
}
Loading