Skip to content

Commit 73183b4

Browse files
authored
Merge pull request #3400 from Gedochao/feature/scalafix-follow-up-2
Merge `scalafix` into `fix`
2 parents a0d3dd7 + 9a45a8b commit 73183b4

26 files changed

+855
-700
lines changed

modules/cli/src/main/scala/scala/cli/ScalaCliCommands.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ class ScalaCliCommands(
3939
export0.Export,
4040
fix.Fix,
4141
fmt.Fmt,
42-
scalafix.Scalafix,
4342
new HelpCmd(help),
4443
installcompletions.InstallCompletions,
4544
installhome.InstallHome,
Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
package scala.cli.commands.fix
2+
3+
import caseapp.core.RemainingArgs
4+
import os.{BasePathImpl, FilePath}
5+
6+
import scala.build.Ops.EitherMap2
7+
import scala.build.errors.{BuildException, CompositeBuildException}
8+
import scala.build.input.*
9+
import scala.build.internal.Constants
10+
import scala.build.options.{BuildOptions, Scope, SuppressWarningOptions}
11+
import scala.build.preprocessing.directives.*
12+
import scala.build.preprocessing.{ExtractedDirectives, SheBang}
13+
import scala.build.{BuildThreads, CrossSources, Logger, Position, Sources}
14+
import scala.cli.commands.shared.SharedOptions
15+
import scala.cli.commands.util.CommandHelpers
16+
import scala.cli.commands.{ScalaCommand, SpecificationLevel}
17+
import scala.cli.config.Keys
18+
import scala.cli.util.ConfigDbUtils
19+
import scala.collection.immutable.HashMap
20+
import scala.util.chaining.scalaUtilChainingOps
21+
22+
object BuiltInRules extends CommandHelpers {
23+
private lazy val targetDirectivesKeysSet = DirectivesPreprocessingUtils.requireDirectiveHandlers
24+
.flatMap(_.keys.flatMap(_.nameAliases)).toSet
25+
private lazy val usingDirectivesKeysGrouped = DirectivesPreprocessingUtils.usingDirectiveHandlers
26+
.flatMap(_.keys)
27+
private lazy val usingDirectivesWithTestPrefixKeysGrouped =
28+
DirectivesPreprocessingUtils.usingDirectiveWithReqsHandlers
29+
.flatMap(_.keys)
30+
31+
private val newLine: String = System.lineSeparator()
32+
33+
def runRules(
34+
inputs: Inputs,
35+
logger: Logger
36+
)(using ScalaCliInvokeData): Unit = {
37+
val (mainSources, testSources) = getProjectSources(inputs, logger)
38+
.left.map(CompositeBuildException(_))
39+
.orExit(logger)
40+
41+
// Only initial inputs are used, new inputs discovered during processing of
42+
// CrossSources.forInput may be shared between projects
43+
val writableInputs: Seq[OnDisk] = inputs.flattened()
44+
.collect { case onDisk: OnDisk => onDisk }
45+
46+
def isExtractedFromWritableInput(position: Option[Position.File]): Boolean = {
47+
val originOrPathOpt = position.map(_.path)
48+
originOrPathOpt match {
49+
case Some(Right(path)) => writableInputs.exists(_.path == path)
50+
case _ => false
51+
}
52+
}
53+
54+
val projectFileContents = new StringBuilder()
55+
56+
given LoggingUtilities(logger, inputs.workspace)
57+
58+
// Deal with directives from the Main scope
59+
val (directivesFromWritableMainInputs, testDirectivesFromMain) = {
60+
val originalMainDirectives = getExtractedDirectives(mainSources)
61+
.filterNot(hasTargetDirectives)
62+
63+
val transformedMainDirectives = unifyCorrespondingNameAliases(originalMainDirectives)
64+
65+
val allDirectives = for {
66+
transformedMainDirective <- transformedMainDirectives
67+
directive <- transformedMainDirective.directives
68+
} yield directive
69+
70+
val (testScopeDirectives, allMainDirectives) =
71+
allDirectives.partition(_.key.startsWith("test"))
72+
73+
createFormattedLinesAndAppend(allMainDirectives, projectFileContents, isTest = false)
74+
75+
(
76+
transformedMainDirectives.filter(d => isExtractedFromWritableInput(d.position)),
77+
testScopeDirectives
78+
)
79+
}
80+
81+
// Deal with directives from the Test scope
82+
val directivesFromWritableTestInputs: Seq[TransformedTestDirectives] =
83+
if (
84+
testSources.paths.nonEmpty || testSources.inMemory.nonEmpty || testDirectivesFromMain.nonEmpty
85+
) {
86+
val originalTestDirectives = getExtractedDirectives(testSources)
87+
.filterNot(hasTargetDirectives)
88+
89+
val transformedTestDirectives = unifyCorrespondingNameAliases(originalTestDirectives)
90+
.pipe(maybeTransformIntoTestEquivalent)
91+
92+
val allDirectives = for {
93+
directivesWithTestPrefix <- transformedTestDirectives.map(_.withTestPrefix)
94+
directive <- directivesWithTestPrefix ++ testDirectivesFromMain
95+
} yield directive
96+
97+
createFormattedLinesAndAppend(allDirectives, projectFileContents, isTest = true)
98+
99+
transformedTestDirectives
100+
.filter(ttd => isExtractedFromWritableInput(ttd.positions))
101+
}
102+
else Seq(TransformedTestDirectives(Nil, Nil, None))
103+
104+
projectFileContents.append(newLine)
105+
106+
// Write extracted directives to project.scala
107+
logger.message(s"Writing ${Constants.projectFileName}")
108+
os.write.over(inputs.workspace / Constants.projectFileName, projectFileContents.toString)
109+
110+
def isProjectFile(position: Option[Position.File]): Boolean =
111+
position.exists(_.path.contains(inputs.workspace / Constants.projectFileName))
112+
113+
// Remove directives from their original files, skip the project.scala file
114+
directivesFromWritableMainInputs
115+
.filterNot(e => isProjectFile(e.position))
116+
.foreach(d => removeDirectivesFrom(d.position))
117+
directivesFromWritableTestInputs
118+
.filterNot(ttd => isProjectFile(ttd.positions))
119+
.foreach(ttd => removeDirectivesFrom(ttd.positions, toKeep = ttd.noTestPrefixAvailable))
120+
121+
}
122+
private def getProjectSources(inputs: Inputs, logger: Logger)(using
123+
ScalaCliInvokeData
124+
): Either[::[BuildException], (Sources, Sources)] = {
125+
val buildOptions = BuildOptions()
126+
127+
val (crossSources, _) = CrossSources.forInputs(
128+
inputs,
129+
preprocessors = Sources.defaultPreprocessors(
130+
buildOptions.archiveCache,
131+
buildOptions.internal.javaClassNameVersionOpt,
132+
() => buildOptions.javaHome().value.javaCommand
133+
),
134+
logger = logger,
135+
suppressWarningOptions = SuppressWarningOptions.suppressAll,
136+
exclude = buildOptions.internal.exclude
137+
).orExit(logger)
138+
139+
val sharedOptions = crossSources.sharedOptions(buildOptions)
140+
val scopedSources = crossSources.scopedSources(sharedOptions).orExit(logger)
141+
142+
val mainSources = scopedSources.sources(Scope.Main, sharedOptions, inputs.workspace, logger)
143+
val testSources = scopedSources.sources(Scope.Test, sharedOptions, inputs.workspace, logger)
144+
145+
(mainSources, testSources).traverseN
146+
}
147+
148+
private def getExtractedDirectives(sources: Sources)(
149+
using loggingUtilities: LoggingUtilities
150+
): Seq[ExtractedDirectives] = {
151+
val logger = loggingUtilities.logger
152+
153+
val fromPaths = sources.paths.map { (path, _) =>
154+
val (_, content) = SheBang.partitionOnShebangSection(os.read(path))
155+
logger.debug(s"Extracting directives from ${loggingUtilities.relativePath(path)}")
156+
ExtractedDirectives.from(content.toCharArray, Right(path), logger, _ => None).orExit(logger)
157+
}
158+
159+
val fromInMemory = sources.inMemory.map { inMem =>
160+
val originOrPath = inMem.originalPath.map((_, path) => path)
161+
val content = originOrPath match {
162+
case Right(path) =>
163+
logger.debug(s"Extracting directives from ${loggingUtilities.relativePath(path)}")
164+
os.read(path)
165+
case Left(origin) =>
166+
logger.debug(s"Extracting directives from $origin")
167+
inMem.wrapperParamsOpt match {
168+
// In case of script snippets, we need to drop the top wrapper lines
169+
case Some(wrapperParams) => String(inMem.content)
170+
.linesWithSeparators
171+
.drop(wrapperParams.topWrapperLineCount)
172+
.mkString
173+
case None => String(inMem.content)
174+
}
175+
}
176+
177+
val (_, contentWithNoShebang) = SheBang.partitionOnShebangSection(content)
178+
179+
ExtractedDirectives.from(
180+
contentWithNoShebang.toCharArray,
181+
originOrPath,
182+
logger,
183+
_ => None
184+
).orExit(logger)
185+
}
186+
187+
fromPaths ++ fromInMemory
188+
}
189+
190+
private def hasTargetDirectives(extractedDirectives: ExtractedDirectives): Boolean = {
191+
// Filter out all elements that contain using target directives
192+
val directivesInElement = extractedDirectives.directives.map(_.key)
193+
directivesInElement.exists(key => targetDirectivesKeysSet.contains(key))
194+
}
195+
196+
private def unifyCorrespondingNameAliases(extractedDirectives: Seq[ExtractedDirectives]) =
197+
extractedDirectives.map { extracted =>
198+
// All keys that we migrate, not all in general
199+
val allKeysGrouped = usingDirectivesKeysGrouped ++ usingDirectivesWithTestPrefixKeysGrouped
200+
val strictDirectives = extracted.directives
201+
202+
val strictDirectivesWithNewKeys = strictDirectives.flatMap { strictDir =>
203+
val newKeyOpt = allKeysGrouped.find(_.nameAliases.contains(strictDir.key))
204+
.flatMap(_.nameAliases.headOption)
205+
.map { key =>
206+
if (key.startsWith("test"))
207+
val withTestStripped = key.stripPrefix("test").stripPrefix(".")
208+
"test." + withTestStripped.take(1).toLowerCase + withTestStripped.drop(1)
209+
else
210+
key
211+
}
212+
213+
newKeyOpt.map(newKey => strictDir.copy(key = newKey))
214+
}
215+
216+
extracted.copy(directives = strictDirectivesWithNewKeys)
217+
}
218+
219+
/** Transforms directives into their 'test.' equivalent if it exists
220+
*
221+
* @param extractedDirectives
222+
* @return
223+
* an instance of TransformedTestDirectives containing transformed directives and those that
224+
* could not be transformed since they have no 'test.' equivalent
225+
*/
226+
private def maybeTransformIntoTestEquivalent(extractedDirectives: Seq[ExtractedDirectives])
227+
: Seq[TransformedTestDirectives] =
228+
for {
229+
extractedFromSingleElement <- extractedDirectives
230+
directives = extractedFromSingleElement.directives
231+
} yield {
232+
val (withTestEquivalent, noTestEquivalent) = directives.partition { directive =>
233+
usingDirectivesWithTestPrefixKeysGrouped.exists(
234+
_.nameAliases.contains("test." + directive.key)
235+
)
236+
}
237+
238+
val transformedToTestEquivalents = withTestEquivalent.map {
239+
case StrictDirective(key, values, _) => StrictDirective("test." + key, values)
240+
}
241+
242+
TransformedTestDirectives(
243+
withTestPrefix = transformedToTestEquivalents,
244+
noTestPrefixAvailable = noTestEquivalent,
245+
positions = extractedFromSingleElement.position
246+
)
247+
}
248+
249+
private def removeDirectivesFrom(
250+
position: Option[Position.File],
251+
toKeep: Seq[StrictDirective] = Nil
252+
)(
253+
using loggingUtilities: LoggingUtilities
254+
): Unit = {
255+
position match {
256+
case Some(Position.File(Right(path), _, _, offset)) =>
257+
val (shebangSection, strippedContent) = SheBang.partitionOnShebangSection(os.read(path))
258+
259+
def ignoreOrAddNewLine(str: String) = if str.isBlank then "" else str + newLine
260+
261+
val keepLines = ignoreOrAddNewLine(shebangSection) + ignoreOrAddNewLine(toKeep.mkString(
262+
"",
263+
newLine,
264+
newLine
265+
))
266+
val newContents = keepLines + strippedContent.drop(offset).stripLeading()
267+
val relativePath = loggingUtilities.relativePath(path)
268+
269+
loggingUtilities.logger.message(s"Removing directives from $relativePath")
270+
if (toKeep.nonEmpty) {
271+
loggingUtilities.logger.message(" Keeping:")
272+
toKeep.foreach(d => loggingUtilities.logger.message(s" $d"))
273+
}
274+
275+
os.write.over(path, newContents.stripLeading())
276+
case _ => ()
277+
}
278+
}
279+
280+
private def createFormattedLinesAndAppend(
281+
strictDirectives: Seq[StrictDirective],
282+
projectFileContents: StringBuilder,
283+
isTest: Boolean
284+
): Unit = {
285+
if (strictDirectives.nonEmpty) {
286+
projectFileContents
287+
.append(if (projectFileContents.nonEmpty) newLine else "")
288+
.append(if isTest then "// Test" else "// Main")
289+
.append(newLine)
290+
291+
strictDirectives
292+
// group by key to merge values
293+
.groupBy(_.key)
294+
.map { (key, directives) =>
295+
StrictDirective(key, directives.flatMap(_.values))
296+
}
297+
// group by key prefixes to create splits between groups
298+
.groupBy(dir => (if (isTest) dir.key.stripPrefix("test.") else dir.key).takeWhile(_ != '.'))
299+
.map { (_, directives) =>
300+
directives.flatMap(_.explodeToStringsWithColLimit()).toSeq.sorted
301+
}
302+
.toSeq
303+
.filter(_.nonEmpty)
304+
.sortBy(_.head)(using directivesOrdering)
305+
// append groups to the StringBuilder, add new lines between groups that are bigger than one line
306+
.foldLeft(0) { (lastSize, directiveLines) =>
307+
val newSize = directiveLines.size
308+
if (lastSize > 1 || (lastSize != 0 && newSize > 1)) projectFileContents.append(newLine)
309+
310+
directiveLines.foreach(projectFileContents.append(_).append(newLine))
311+
312+
newSize
313+
}
314+
}
315+
}
316+
317+
private case class TransformedTestDirectives(
318+
withTestPrefix: Seq[StrictDirective],
319+
noTestPrefixAvailable: Seq[StrictDirective],
320+
positions: Option[Position.File]
321+
)
322+
323+
private case class LoggingUtilities(
324+
logger: Logger,
325+
workspacePath: os.Path
326+
) {
327+
def relativePath(path: os.Path): FilePath & BasePathImpl =
328+
if (path.startsWith(workspacePath)) path.relativeTo(workspacePath)
329+
else path
330+
}
331+
332+
private val directivesOrdering: Ordering[String] = {
333+
def directivesOrder(key: String): Int = {
334+
val handlersOrder = Seq(
335+
ScalaVersion.handler.keys,
336+
Platform.handler.keys,
337+
Jvm.handler.keys,
338+
JavaHome.handler.keys,
339+
ScalaNative.handler.keys,
340+
ScalaJs.handler.keys,
341+
ScalacOptions.handler.keys,
342+
JavaOptions.handler.keys,
343+
JavacOptions.handler.keys,
344+
JavaProps.handler.keys,
345+
MainClass.handler.keys,
346+
scala.build.preprocessing.directives.Sources.handler.keys,
347+
ObjectWrapper.handler.keys,
348+
Toolkit.handler.keys,
349+
Dependency.handler.keys
350+
)
351+
352+
handlersOrder.zipWithIndex
353+
.find(_._1.flatMap(_.nameAliases).contains(key))
354+
.map(_._2)
355+
.getOrElse(if key.startsWith("publish") then 20 else 15)
356+
}
357+
358+
Ordering.by { directiveLine =>
359+
val key = directiveLine
360+
.stripPrefix("//> using")
361+
.stripLeading()
362+
.stripPrefix("test.")
363+
// separate key from value
364+
.takeWhile(!_.isWhitespace)
365+
366+
directivesOrder(key)
367+
}
368+
}
369+
}

0 commit comments

Comments
 (0)