("Field must be empty or an integer in the range 0 to 12") {
151 | val value = it
152 | value.isNotEmpty() && value.any { digit -> !digit.isDigit() } ||
153 | (value.toIntOrNull() == null || value.toInt() > 12)
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/library/src/main/kotlin/com/facebook/ktfmt/kdoc/KDocFormatter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) Tor Norbye.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.facebook.ktfmt.kdoc
18 |
19 | import kotlin.math.min
20 |
21 | /** Formatter which can reformat KDoc comments. */
22 | class KDocFormatter(private val options: KDocFormattingOptions) {
23 | /** Reformats the [comment], which follows the given [initialIndent] string. */
24 | fun reformatComment(comment: String, initialIndent: String): String {
25 | return reformatComment(FormattingTask(options, comment, initialIndent))
26 | }
27 |
28 | fun reformatComment(task: FormattingTask): String {
29 | val indent = task.secondaryIndent
30 | val indentSize = getIndentSize(indent, options)
31 | val firstIndentSize = getIndentSize(task.initialIndent, options)
32 | val comment = task.comment
33 | val lineComment = comment.isLineComment()
34 | val blockComment = comment.isBlockComment()
35 | val paragraphs = ParagraphListBuilder(comment, options, task).scan(indentSize)
36 | val commentType = task.type
37 | val lineSeparator = "\n$indent${commentType.linePrefix}"
38 | val prefix = commentType.prefix
39 |
40 | // Collapse single line? If alternate is turned on, use the opposite of the
41 | // setting
42 | val collapseLine = options.collapseSingleLine.let { if (options.alternate) !it else it }
43 | if (paragraphs.isSingleParagraph() && collapseLine && !lineComment) {
44 | // Does the text fit on a single line?
45 | val trimmed = paragraphs.firstOrNull()?.text?.trim() ?: ""
46 | // Subtract out space for "/** " and " */" and the indent:
47 | val width =
48 | min(
49 | options.maxLineWidth - firstIndentSize - commentType.singleLineOverhead(),
50 | options.maxCommentWidth)
51 | val suffix = if (commentType.suffix.isEmpty()) "" else " ${commentType.suffix}"
52 | if (trimmed.length <= width) {
53 | return "$prefix $trimmed$suffix"
54 | }
55 | if (indentSize < firstIndentSize) {
56 | val nextLineWidth =
57 | min(
58 | options.maxLineWidth - indentSize - commentType.singleLineOverhead(),
59 | options.maxCommentWidth)
60 | if (trimmed.length <= nextLineWidth) {
61 | return "$prefix $trimmed$suffix"
62 | }
63 | }
64 | }
65 |
66 | val sb = StringBuilder()
67 |
68 | sb.append(prefix)
69 | if (lineComment) {
70 | sb.append(' ')
71 | } else {
72 | sb.append(lineSeparator)
73 | }
74 |
75 | for (paragraph in paragraphs) {
76 | if (paragraph.separate) {
77 | // Remove trailing spaces which can happen when we have a paragraph
78 | // separator
79 | stripTrailingSpaces(lineComment, sb)
80 | sb.append(lineSeparator)
81 | }
82 | val text = paragraph.text
83 | if (paragraph.preformatted || paragraph.table) {
84 | sb.append(text)
85 | // Remove trailing spaces which can happen when we have an empty line in a
86 | // preformatted paragraph.
87 | stripTrailingSpaces(lineComment, sb)
88 | sb.append(lineSeparator)
89 | continue
90 | }
91 |
92 | val lineWithoutIndent = options.maxLineWidth - commentType.lineOverhead()
93 | val quoteAdjustment = if (paragraph.quoted) 2 else 0
94 | val maxLineWidth =
95 | min(options.maxCommentWidth, lineWithoutIndent - indentSize) - quoteAdjustment
96 | val firstMaxLineWidth =
97 | if (sb.indexOf('\n') == -1) {
98 | min(options.maxCommentWidth, lineWithoutIndent - firstIndentSize) - quoteAdjustment
99 | } else {
100 | maxLineWidth
101 | }
102 |
103 | val lines = paragraph.reflow(firstMaxLineWidth, maxLineWidth)
104 | var first = true
105 | val hangingIndent = paragraph.hangingIndent
106 | for (line in lines) {
107 | sb.append(paragraph.indent)
108 | if (first && !paragraph.continuation) {
109 | first = false
110 | } else {
111 | sb.append(hangingIndent)
112 | }
113 | if (paragraph.quoted) {
114 | sb.append("> ")
115 | }
116 | if (line.isEmpty()) {
117 | // Remove trailing spaces which can happen when we have a paragraph
118 | // separator
119 | stripTrailingSpaces(lineComment, sb)
120 | } else {
121 | sb.append(line)
122 | }
123 | sb.append(lineSeparator)
124 | }
125 | }
126 | if (!lineComment) {
127 | if (sb.endsWith("* ")) {
128 | sb.setLength(sb.length - 2)
129 | }
130 | sb.append("*/")
131 | } else if (sb.endsWith(lineSeparator)) {
132 | @Suppress("ReturnValueIgnored") sb.removeSuffix(lineSeparator)
133 | }
134 |
135 | val formatted =
136 | if (lineComment) {
137 | sb.trim().removeSuffix("//").trim().toString()
138 | } else if (blockComment) {
139 | sb.toString().replace(lineSeparator + "\n", "\n\n")
140 | } else {
141 | sb.toString()
142 | }
143 |
144 | val separatorIndex = comment.indexOf('\n')
145 | return if (separatorIndex > 0 && comment[separatorIndex - 1] == '\r') {
146 | // CRLF separator
147 | formatted.replace("\n", "\r\n")
148 | } else {
149 | formatted
150 | }
151 | }
152 |
153 | private fun stripTrailingSpaces(lineComment: Boolean, sb: StringBuilder) {
154 | if (!lineComment && sb.endsWith("* ")) {
155 | sb.setLength(sb.length - 1)
156 | } else if (lineComment && sb.endsWith("// ")) {
157 | sb.setLength(sb.length - 1)
158 | }
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/ide-plugin/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # KDoc Formatter Plugin Changelog
4 |
5 | ## [1.6.9]
6 | - Add support for 2025.3 EAP
7 |
8 | ## [1.6.8]
9 | - Add support for IJP 2025.2 EAP
10 |
11 | ## [1.6.7]
12 | - Updated code to replace deprecated API usages
13 | - Fix issue #104: Include type parameters in parameter list reordering
14 | - Fix issue #105: Make add punctuation apply to all paragraphs, not just last
15 |
16 | ## [1.6.6]
17 | - Add support for 2025.1 EAP, and migrate to 2.x version of IntelliJ gradle plugin.
18 | - Fix https://github.com/tnorbye/kdoc-formatter/issues/106
19 | - Allow line comment block reformatting to work at the end of lines
20 |
21 | ## [1.6.5]
22 | - Mark plugin as compatible with 2024.3.
23 |
24 | ## [1.6.4]
25 | - Switch continuation indent from 4 to 3. (IntelliJ's Dokka preview
26 | treats an indent of 4 or more as preformatted text even on a continued
27 | line; Dokka itself (and Markdown) does not.
28 | - Add ability to override the continuation indent in the IDE plugin
29 | settings.
30 | - Don't reorder `@sample` tags (backported
31 | https://github.com/facebook/ktfmt/issues/406)
32 |
33 | ## [1.6.3]
34 | - Compatibility with IntelliJ 2024.2 EAP
35 | - Mark plugin as compatible with K2
36 |
37 | ## [1.6.2]
38 | - Compatibility with IntelliJ 2024.1 EAP.
39 |
40 | ## [1.6.1]
41 | - Compatibility with IntelliJ 2023.3 EAP.
42 |
43 | ## [1.6.0]
44 | - Updated dependencies and fixed a few minor bugs, including
45 | https://github.com/tnorbye/kdoc-formatter/issues/88 as well as issue
46 | 398 in ktfmt.
47 |
48 | ## [1.5.9]
49 | - Compatibility with IntelliJ 2023.1
50 |
51 | ## [1.5.8]
52 | - Fixed a number of bugs:
53 | - #84: Line overrun when using closed-open interval notation
54 | - More gracefully handle unterminated [] references (for example when
55 | comment is using it in things like [closed, open) intervals)
56 | - Recognize and convert accidentally capitalized kdoc tags like @See
57 | - If you have a [ref] which spans a line such that the # ends up as a
58 | new line, don't treat this as a "# heading".
59 | - If you're using optimal line breaking and there's a really long,
60 | unbreakable word in the paragraph, switch that paragraph over to
61 | greedy line breaking (to make the paragraph better balanced since
62 | the really long word throws the algorithm off.)
63 | - Fix a few scenarios where markup conversion from and
64 | wasn't converting everything.
65 | - Allow @property[name], not just @param[name]
66 | - Some minor code cleanup.
67 |
68 | ## [1.5.7]
69 | - Fixed the following bugs:
70 | - #76: Preserve newline style (CRLF on Windows)
71 | - #77: Preformatting error
72 | - #78: Preformatting stability
73 | - #79: Replace `{@param name}` with `[name]`
74 |
75 | ## [1.5.6]
76 | - Bugfix: the override line width setting was not working
77 |
78 | ## [1.5.5]
79 | - The plugin can now be upgraded without restarting the IDE
80 | - Improved support for .editorconfig files; these settings will now be
81 | reflected immediately (in prior versions you had to restart the IDE
82 | because they were improperly cached)
83 | - Fixed a copy/paste bug which prevented the "Collapse short comments
84 | that fit on a single line" option from working.
85 | - Several formatting related improvements (fixes for
86 | bugs #53, #69, #70, #71, #72)
87 |
88 | ## [1.5.4]
89 | - Fix 9 bugs filed by the ktfmt project.
90 |
91 | ## [1.5.3]
92 | - @param tags are reordered to match the parameter order in the
93 | corresponding method signature.
94 | - There are now options for explicitly specifying the line width and the
95 | comment width which overrides the inferred width from code styles or
96 | .editorconfig files.
97 | - Some reorganization of the options along with updates labels to
98 | clarify what they mean.
99 |
100 | ## [1.5.2]
101 | - Adds a new option which lets you turn off the concept of a separate
102 | maximum comment width from the maximum line width. By default,
103 | comments are limited to 72 characters wide (or more accurately the
104 | configured width for Markdown files), which leads to more readable
105 | text. However, if you really want the full line width to be used,
106 | uncheck the "Allow max comment width to be separate from line width"
107 | setting.
108 | - Fixes bug to ensure the line width and max comment width are properly
109 | read from the IDE environment settings.
110 |
111 | ## [1.5.1]
112 | - Updated formatter with many bug fixes, as well as improved support for
113 | formatting tables as well as reordering KDoc tags. There are new
114 | options controlling both of these behaviors.
115 | - Removed Kotlin logo from the IDE plugin icon
116 |
117 | ## [1.4.1]
118 | - Fix formatting nested bulleted lists
119 | (https://github.com/tnorbye/kdoc-formatter/issues/36)
120 |
121 | ## [1.4.0]
122 | - The KDoc formatter now participates in regular IDE source code
123 | formatting (e.g. Code > Reformat Code). This can be turned off via a
124 | setting.
125 | - The markup conversion (if enabled) now converts [] and {@linkplain}
126 | tags to the KDoc equivalent.
127 | - Fix bug where preformatted text immediately following a TODO comment
128 | would be joined into the TODO.
129 |
130 | ## [1.3.3]
131 | - Don't break lines inside link text which will include the comment
132 | asterisk
133 | - Allow formatting during indexing
134 |
135 | ## [1.3.2]
136 | - Bugfixes and update deprecated API usage for 2021.2 compatibility
137 |
138 | ## [1.3.1]
139 | - Fixes a few bugs around markup conversion not producing the right
140 | number of blank lines for a , and adds a few more prefixes as
141 | non-breakable (e.g. if you have an em dash in your sentence -- like
142 | this -- we don't want the "--" to be placed at the beginning of a
143 | line.)
144 | - Adds an --add-punctuation command line flag and IDE setting to
145 | optionally add closing periods on capitalized paragraphs at the end of
146 | the comment.
147 | - Special cases TODO: comments (placing them in a block by themselves
148 | and using hanging indents).
149 |
150 | ## [1.3.0]
151 | - Many improves to the markdown handling, such as quoted blocks,
152 | headers, list continuations, etc.
153 | - Markup conversion, which until this point could convert inline tags
154 | such as **bold** into **bold**, etc, now handles many block level tags
155 | too, such as \
, \
, etc.
156 | - The IDE plugin can now also reformat line comments under the caret.
157 | (This is opt-in via options.)
158 |
159 | ## [1.2.0]
160 | - IDE settings panel
161 | - Ability to alternate formatting between greedy and optimal line
162 | breaking when invoked repeatedly (and for short comments, alternating
163 | between single line and multiple lines.)
164 |
165 | ## [1.1.2]
166 | - Basic support for .editorconfig files.
167 |
168 | ## [1.1.0]
169 | - Support for setting maximum comment width (capped by the maximum line
170 | width).
171 |
172 | ## [1.0.0]
173 | - Initial version
174 |
--------------------------------------------------------------------------------
/cli/src/main/kotlin/kdocformatter/cli/GitRangeFilter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) Tor Norbye.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package kdocformatter.cli
18 |
19 | import java.io.BufferedReader
20 | import java.io.File
21 | import java.io.InputStreamReader
22 | import java.nio.file.Files
23 |
24 | /**
25 | * Searches the current git commit for modified regions and returns these.
26 | *
27 | * TODO: Allow specifying an arbitrary range of git sha's. This requires some work to figure out the
28 | * correct line numbers in the current version from older patches.
29 | */
30 | class GitRangeFilter private constructor(rangeMap: RangeMap) : LineRangeFilter(rangeMap) {
31 | companion object {
32 | fun create(gitPath: String, fileInRepository: File, staged: Boolean = false): GitRangeFilter? {
33 | val git = findGit(gitPath) ?: return null
34 | val args = mutableListOf()
35 | val gitRepo = findGitRepo(fileInRepository) ?: return null
36 | val output = Files.createTempFile("gitshow", ".diff").toFile()
37 | args.add(git.path)
38 | args.add("--git-dir=$gitRepo")
39 | args.add("--no-pager")
40 | if (staged) {
41 | args.add("diff")
42 | args.add("--cached")
43 | } else {
44 | args.add("show")
45 | }
46 | args.add("--no-color")
47 | args.add("--no-prefix")
48 | args.add("--unified=0")
49 | args.add("--output=$output")
50 | if (!executeProcess(args)) {
51 | return null
52 | }
53 | val root = gitRepo.parentFile
54 | val diff = output.readText()
55 | return create(root, diff)
56 | }
57 |
58 | /**
59 | * Creates range from the given diff contents. Extracted to be separate from the git invocation
60 | * above for unit test purposes.
61 | */
62 | fun create(root: File?, diff: String): GitRangeFilter {
63 | val rangeMap = RangeMap()
64 | var currentPath = root
65 | for (line in diff.split("\n")) {
66 | if (line.startsWith("+++ ")) {
67 | val relative = line.substring(4)
68 | // Canonicalize files here to match the canonicalization we perform in
69 | // KDocFileFormattingOptions.parse (which is necessary such that we don't
70 | // accidentally handle relative paths like "./" etc as "foo/./bar" which
71 | // isn't treated as equal to "foo/bar").
72 | currentPath = (if (root != null) File(root, relative) else File(relative)).canonicalFile
73 | } else if (line.startsWith("@@ ")) {
74 | //noinspection FileComparisons
75 | if (currentPath === root || currentPath == null || !currentPath.path.endsWith(".kt")) {
76 | continue
77 | }
78 | val rangeStart = line.indexOf('+') + 1
79 | val rangeEnd = line.indexOf(' ', rangeStart + 1)
80 | val range = line.substring(rangeStart, rangeEnd)
81 | val lineCountStart = range.indexOf(",")
82 | val startLine: Int
83 | val lineCount: Int
84 | if (lineCountStart == -1) {
85 | startLine = range.toInt()
86 | lineCount = 1
87 | } else {
88 | startLine = range.substring(0, lineCountStart).toInt()
89 | lineCount = range.substring(lineCountStart + 1).toInt()
90 | }
91 | rangeMap.addRange(currentPath, startLine, startLine + lineCount)
92 | }
93 | }
94 |
95 | return GitRangeFilter(rangeMap)
96 | }
97 |
98 | private fun executeProcess(args: List): Boolean {
99 | try {
100 | val process = Runtime.getRuntime().exec(args.toTypedArray())
101 | val input = BufferedReader(InputStreamReader(process.inputStream))
102 | val error = BufferedReader(InputStreamReader(process.errorStream))
103 | val exitVal = process.waitFor()
104 | if (exitVal != 0) {
105 | val sb = StringBuilder()
106 | sb.append("Failed to run git command.\n")
107 | sb.append("Command args:\n")
108 | for (arg in args) {
109 | sb.append(" ").append(arg).append("\n")
110 | }
111 | sb.append("Standard output:\n")
112 | var line: String?
113 | while (input.readLine().also { line = it } != null) {
114 | sb.append(line).append("\n")
115 | }
116 | sb.append("Error output:\n")
117 | while (error.readLine().also { line = it } != null) {
118 | sb.append(line).append("\n")
119 | }
120 | input.close()
121 | error.close()
122 | System.err.println(sb.toString())
123 | return false
124 | }
125 | return true
126 | } catch (t: Throwable) {
127 | val sb = StringBuilder()
128 | for (arg in args) {
129 | sb.append(" ").append(arg).append("\n")
130 | }
131 | System.err.println(sb.toString())
132 | return false
133 | }
134 | }
135 |
136 | /** Returns the .git folder for the [file] or directory somewhere in the report. */
137 | private fun findGitRepo(file: File): File? {
138 | var curr = file.absoluteFile
139 | while (true) {
140 | val git = File(curr, ".git")
141 | if (git.isDirectory) {
142 | return git
143 | }
144 | curr = curr.parentFile ?: return null
145 | }
146 | }
147 |
148 | private fun findGit(gitPath: String): File? {
149 | if (gitPath.isNotEmpty()) {
150 | val file = File(gitPath)
151 | return if (file.exists()) {
152 | file
153 | } else {
154 | System.err.println("$gitPath does not exist")
155 | null
156 | }
157 | }
158 |
159 | val git = findOnPath("git") ?: findOnPath("git.exe")
160 | if (git != null) {
161 | val gitFile = File(git)
162 | if (!gitFile.canExecute()) {
163 | System.err.println("Cannot execute $gitFile")
164 | return null
165 | }
166 | return gitFile
167 | } else {
168 | return null
169 | }
170 | }
171 |
172 | private fun findOnPath(target: String): String? {
173 | val path = System.getenv("PATH")?.split(File.pathSeparator) ?: return null
174 | for (binDir in path) {
175 | val file = File(binDir + File.separator + target)
176 | if (file.isFile) { // maybe file.canExecute() too but not sure how .bat files behave
177 | return file.path
178 | }
179 | }
180 | return null
181 | }
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/library/src/test/kotlin/com/facebook/ktfmt/kdoc/UtilitiesTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) Tor Norbye.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.facebook.ktfmt.kdoc
18 |
19 | import com.google.common.truth.Truth.assertThat
20 | import org.junit.Test
21 | import org.junit.runner.RunWith
22 | import org.junit.runners.JUnit4
23 |
24 | @RunWith(JUnit4::class)
25 | class UtilitiesTest {
26 | @Test
27 | fun testFindSamePosition() {
28 | fun check(newWithCaret: String, oldWithCaret: String) {
29 | val oldCaretIndex = oldWithCaret.indexOf('|')
30 | val newCaretIndex = newWithCaret.indexOf('|')
31 | assertThat(oldCaretIndex != -1).isTrue()
32 | assertThat(newCaretIndex != -1).isTrue()
33 | val old = oldWithCaret.substring(0, oldCaretIndex) + oldWithCaret.substring(oldCaretIndex + 1)
34 | val new = newWithCaret.substring(0, newCaretIndex) + newWithCaret.substring(newCaretIndex + 1)
35 | val newPos = findSamePosition(old, oldCaretIndex, new)
36 |
37 | val actual = new.substring(0, newPos) + "|" + new.substring(newPos)
38 | assertThat(actual).isEqualTo(newWithCaret)
39 | }
40 |
41 | // Prefix match
42 | check("|/** Test\n Different Middle End */", "|/** Test2 End */")
43 | check("/|** Test\n Different Middle End */", "/|** Test2 End */")
44 | check("/*|* Test\n Different Middle End */", "/*|* Test2 End */")
45 | check("/**| Test\n Different Middle End */", "/**| Test2 End */")
46 | check("/** |Test\n Different Middle End */", "/** |Test2 End */")
47 | check("/** T|est\n Different Middle End */", "/** T|est2 End */")
48 | check("/** Te|st\n Different Middle End */", "/** Te|st2 End */")
49 | check("/** Tes|t\n Different Middle End */", "/** Tes|t2 End */")
50 | check("/** Test|\n Different Middle End */", "/** Test|2 End */")
51 | // End match
52 | check("/** Test\n Different Middle| End */", "/** Test2| End */")
53 | check("/** Test\n Different Middle E|nd */", "/** Test2 E|nd */")
54 | check("/** Test\n Different Middle En|d */", "/** Test2 En|d */")
55 | check("/** Test\n Different Middle End| */", "/** Test2 End| */")
56 | check("/** Test\n Different Middle End |*/", "/** Test2 End |*/")
57 | check("/** Test\n Different Middle End *|/", "/** Test2 End *|/")
58 | check("/** Test\n Different Middle End */|", "/** Test2 End */|")
59 |
60 | check("|/**\nTest End\n*/", "|/** Test End */")
61 | check("/|**\nTest End\n*/", "/|** Test End */")
62 | check("/*|*\nTest End\n*/", "/*|* Test End */")
63 | check("/**|\nTest End\n*/", "/**| Test End */")
64 | check("/**\n|Test End\n*/", "/** |Test End */")
65 | check("/**\nT|est End\n*/", "/** T|est End */")
66 | check("/**\nTe|st End\n*/", "/** Te|st End */")
67 | check("/**\nTes|t End\n*/", "/** Tes|t End */")
68 | check("/**\nTest| End\n*/", "/** Test| End */")
69 | check("/**\nTest |End\n*/", "/** Test |End */")
70 | check("/**\nTest E|nd\n*/", "/** Test E|nd */")
71 | check("/**\nTest En|d\n*/", "/** Test En|d */")
72 | check("/**\nTest End|\n*/", "/** Test End| */")
73 | check("/**\nTest End\n|*/", "/** Test End |*/")
74 | check("/**\nTest End\n*|/", "/** Test End *|/")
75 | check("/**\nTest End\n*/|", "/** Test End */|")
76 |
77 | check("|/** Test End */", "|/** Test2 End */")
78 | check("/|** Test End */", "/|** Test2 End */")
79 | check("/*|* Test End */", "/*|* Test2 End */")
80 | check("/**| Test End */", "/**| Test2 End */")
81 | check("/** |Test End */", "/** |Test2 End */")
82 | check("/** T|est End */", "/** T|est2 End */")
83 | check("/** Te|st End */", "/** Te|st2 End */")
84 | check("/** Tes|t End */", "/** Tes|t2 End */")
85 | check("/** Test| End */", "/** Test|2 End */")
86 | check("/** Test |End */", "/** Test2 |End */")
87 | check("/** Test E|nd */", "/** Test2 E|nd */")
88 | check("/** Test En|d */", "/** Test2 En|d */")
89 | check("/** Test End| */", "/** Test2 End| */")
90 | check("/** Test End |*/", "/** Test2 End |*/")
91 | check("/** Test End *|/", "/** Test2 End *|/")
92 | check("/** Test End */|", "/** Test2 End */|")
93 | }
94 |
95 | @Test
96 | fun testGetParamName() {
97 | assertThat("@param foo".getParamName()).isEqualTo("foo")
98 | assertThat("@param foo bar".getParamName()).isEqualTo("foo")
99 | assertThat("@param foo;".getParamName()).isEqualTo("foo")
100 | assertThat(" \t@param\t foo bar.".getParamName()).isEqualTo("foo")
101 | assertThat("@param[foo]".getParamName()).isEqualTo("foo")
102 | assertThat("@param [foo]".getParamName()).isEqualTo("foo")
103 | assertThat("@param ".getParamName()).isNull()
104 | }
105 |
106 | @Test
107 | fun testComputeWords() {
108 | fun List.describe(): String {
109 | return "listOf(${this.joinToString(", ") { "\"$it\"" }})"
110 | }
111 | fun check(text: String, expected: List, customizeParagraph: (Paragraph) -> Unit = {}) {
112 | val task = FormattingTask(KDocFormattingOptions(12), "/** $text */", "")
113 | val paragraph = Paragraph(task)
114 | paragraph.content.append(text)
115 | customizeParagraph(paragraph)
116 | val words = paragraph.computeWords()
117 |
118 | assertThat(words.describe()).isEqualTo(expected.describe())
119 | }
120 | check("Foo", listOf("Foo"))
121 | check("Foo Bar Baz", listOf("Foo", "Bar", "Baz"))
122 | check("Foo Bar Baz", listOf("Foo Bar", "Baz")) { it.quoted = true }
123 | check("Foo Bar Baz", listOf("Foo Bar", "Baz")) { it.hanging = true }
124 | check("1. Foo", listOf("1.", "Foo"))
125 | // "1." can't start a word; if it ends up at the beginning of a line it becomes
126 | // a numbered element.
127 | check("Foo 1.", listOf("Foo 1."))
128 | check("Foo bar [Link Text] foo bar.", listOf("Foo", "bar", "[Link Text]", "foo", "bar."))
129 | check("Interval [0, 1) foo bar.", listOf("Interval [0, 1)", "foo", "bar."))
130 |
131 | // ">" cannot start a word; it would become quoted text
132 | check("if >= 3", listOf("if >=", "3"))
133 | check("if >= 3.", listOf("if >= 3."))
134 |
135 | check(
136 | "SDK version - [`Partial(Mode.UseIfAvailable)`](Partial) on API 24+",
137 | listOf("SDK", "version - [`Partial(Mode.UseIfAvailable)`](Partial)", "on", "API", "24+"))
138 |
139 | check(
140 | "Z orders can range from Integer.MIN_VALUE to Integer.MAX_VALUE. Default z order " +
141 | " index is 0. [SurfaceControlWrapper] instances are positioned back-to-front.",
142 | listOf(
143 | "Z",
144 | "orders",
145 | "can",
146 | "range",
147 | "from",
148 | "Integer.MIN_VALUE",
149 | "to",
150 | "Integer.MAX_VALUE.",
151 | "Default",
152 | "z",
153 | "order",
154 | "index",
155 | "is 0. [SurfaceControlWrapper]",
156 | "instances",
157 | "are",
158 | "positioned",
159 | "back-to-front."))
160 | check(
161 | "Equates to `cmd package compile -f -m speed ` on API 24+.",
162 | listOf(
163 | "Equates",
164 | "to",
165 | "`cmd",
166 | "package",
167 | "compile",
168 | "-f",
169 | "-m",
170 | "speed",
171 | "`",
172 | "on",
173 | "API",
174 | "24+."))
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/library/src/test/kotlin/com/facebook/ktfmt/kdoc/DokkaVerifier.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) Tor Norbye.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | @file:Suppress("PropertyName", "PrivatePropertyName")
18 |
19 | package com.facebook.ktfmt.kdoc
20 |
21 | import com.google.common.truth.Truth.assertThat
22 | import java.io.BufferedReader
23 | import java.io.File
24 |
25 | /**
26 | * Verifies that two KDoc comment strings render to the same HTML documentation using Dokka. This is
27 | * used by the test infrastructure to make sure that the transformations we're allowing are not
28 | * changing the appearance of the documentation.
29 | *
30 | * Unfortunately, just diffing HTML strings isn't always enough, because dokka will preserve some
31 | * text formatting which is immaterial to the HTML appearance. Therefore, if you've also installed
32 | * Pandoc, it will use that to generate a text rendering of the HTML which is then used for diffing
33 | * instead. (Even this isn't fullproof because pandoc also preserves some details that should not
34 | * matter). Text rendering does drop a lot of markup (such as bold and italics) so it would be
35 | * better to compare in some other format, such as PDF, but unfortunately, the PDF rendering doesn't
36 | * appear to be stable; rendering the same document twice yields a binary diff.
37 | *
38 | * Dokka no longer provides a fat/shadow jar; instead you have to download a bunch of different
39 | * dependencies. Therefore, for convenience this is set up to point to an AndroidX checkout, which
40 | * has all the prebuilts. Point the below to AndroidX and the rest should work.
41 | */
42 | class DokkaVerifier(private val tempFolder: File) {
43 | // Configuration parameters
44 | // Checkout of https://github.com/androidx/androidx
45 | private val ANDROIDX_HOME: String? = null
46 |
47 | // Optional install of pandoc, e.g. "/opt/homebrew/bin/pandoc"
48 | private val PANDOC: String? = null
49 |
50 | // JDK install
51 | private val JAVA_HOME: String? = System.getenv("JAVA_HOME") ?: System.getProperty("java.home")
52 |
53 | fun verify(before: String, after: String) {
54 | JAVA_HOME ?: return
55 | ANDROIDX_HOME ?: return
56 |
57 | val androidx = File(ANDROIDX_HOME)
58 | if (!androidx.isDirectory) {
59 | return
60 | }
61 |
62 | val prebuilts = File(androidx, "prebuilts")
63 | if (!prebuilts.isDirectory) {
64 | println("AndroidX prebuilts not found; not verifying with Dokka")
65 | }
66 | val cli = find(prebuilts, "org.jetbrains.dokka", "dokka-cli")
67 | val analysis = find(prebuilts, "org.jetbrains.dokka", "dokka-analysis")
68 | val base = find(prebuilts, "org.jetbrains.dokka", "dokka-base")
69 | val compiler = find(prebuilts, "org.jetbrains.dokka", "kotlin-analysis-compiler")
70 | val intellij = find(prebuilts, "org.jetbrains.dokka", "kotlin-analysis-intellij")
71 | val coroutines = find(prebuilts, "org.jetbrains.kotlinx", "kotlinx-coroutines-core")
72 | val html = find(prebuilts, "org.jetbrains.kotlinx", "kotlinx-html-jvm")
73 | val freemarker = find(prebuilts, "org.freemarker", "freemarker")
74 |
75 | val src = File(tempFolder, "src")
76 | val out = File(tempFolder, "dokka")
77 | src.mkdirs()
78 | out.mkdirs()
79 |
80 | val beforeFile = File(src, "before.kt")
81 | beforeFile.writeText("${before.split("\n").joinToString("\n") { it.trim() }}\nclass Before\n")
82 |
83 | val afterFile = File(src, "after.kt")
84 | afterFile.writeText("${after.split("\n").joinToString("\n") { it.trim() }}\nclass After\n")
85 |
86 | val args = mutableListOf()
87 | args.add(File(JAVA_HOME, "bin/java").path)
88 | args.add("-jar")
89 | args.add(cli.path)
90 | args.add("-pluginsClasspath")
91 | val pathSeparator =
92 | ";" // instead of File.pathSeparator as would have been reasonable (e.g. : on Unix)
93 | val path =
94 | listOf(analysis, base, compiler, intellij, coroutines, html, freemarker).joinToString(
95 | pathSeparator) {
96 | it.path
97 | }
98 | args.add(path)
99 | args.add("-sourceSet")
100 | args.add("-src $src") // (nested parameter within -sourceSet)
101 | args.add("-outputDir")
102 | args.add(out.path)
103 | executeProcess(args)
104 |
105 | fun getHtml(file: File): String {
106 | val rendered = file.readText()
107 | val begin = rendered.indexOf("