forked from AVSystem/scala-commons
-
Notifications
You must be signed in to change notification settings - Fork 0
Analyzer #5
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
Open
halotukozak
wants to merge
39
commits into
master
Choose a base branch
from
analyzer
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Analyzer #5
Changes from 29 commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
e5fa4f5
docs(phase-2): research test infrastructure domain
halotukozak cea1d5e
chore(02-01): add scalatest dependency and Test/fork to analyzer module
halotukozak 16dab56
feat(02-02): rewrite AnalyzerTest for Scala 3 with ContextBase harness
halotukozak aa41335
test(02-03): create CanaryTest validating test infrastructure
halotukozak b99a01c
feat(03-02): implement FinalCaseClasses rule
halotukozak d7f43e6
feat(03-05): implement ImplicitParamDefaults rule with comprehensive …
halotukozak acf0f7c
feat(03-03): implement CatchThrowable rule with Scala 3 type equivale…
halotukozak 3207d16
fix(03-01): fix ImportJavaUtil rule using sel.renamed.isEmpty for cor…
halotukozak 1a0847f
feat(03-04): fix CheckBincompat definition site detection for module …
halotukozak dd7e112
feat(03-03): implement ThrowableObjects rule with Scala 3 subtype and…
halotukozak 9eccf0d
feat(03-05): implement VarargsAtLeast rule with Scala 3 SeqLiteral va…
halotukozak df5a5fd
feat(03-05): implement NothingAsFunctionArgument rule detecting Nothi…
halotukozak 371bef9
feat(03-01): implement BasePackage rule with recursive PackageDef val…
halotukozak c3eda7f
feat(03-04): implement FindUsages rule with symbol name matching and …
halotukozak 50812c0
feat(03-02): implement FinalValueClasses rule with Scala 3 AnyVal det…
halotukozak 11f939e
feat(04-01): implement ConstantDeclarations rule with literal constan…
halotukozak a19da6f
feat(04-04): implement CheckMacroPrivate rule with inline method dete…
halotukozak 896190c
feat(04-03): implement ShowAst rule with tree.show and annotation det…
halotukozak 41af41e
feat(04-03): implement ExplicitGenerics rule with span-based inferred…
halotukozak c0a16cd
feat(04-04): implement DiscardedMonixTask rule with recursive tree tr…
halotukozak e3d3629
feat(04-01): implement ImplicitFunctionParams rule with function type…
halotukozak dd6afee
feat(04-02): implement ImplicitTypes rule with span-based inferred ty…
halotukozak dab4fe4
feat(04-02): implement ImplicitValueClasses rule checking implicit cl…
halotukozak 83c531d
feat(05-01): delete Any2StringAdd and silence unknown rule warnings
halotukozak a682c2d
feat(05-01): implement ValueEnumExhaustiveMatch rule with tests
halotukozak 4ea6f59
feat(05-02): convert implicit def to extension method and apply scalafmt
halotukozak 48b9898
feat: remove FinalValueClasses rule and clean up related test cases
halotukozak cb9e755
refactor: simplify analyzer rules and test sources
halotukozak 56c8440
chore: remove outdated 02-RESEARCH.md for Phase 2 test infrastructure…
halotukozak 37b688c
revert some tests, formatting
halotukozak 6f2a86b
Update analyzer rules and tests: improve validation, add debug loggin…
halotukozak 99b66ac
Refactor `AnalyzerRule` implementations: simplify class definitions b…
halotukozak f841cc2
Refactor `AnalyzerRule`: replace `transform*` methods with `verify*`,…
halotukozak 2f07f4e
Refactor `AnalyzerRule`: replace `Seq` with `List` in `requiredSymbol…
halotukozak 5f875fa
Refactor `AnalyzerTest`: simplify `compile` method by returning `Repo…
halotukozak 230a7e9
Remove `CanaryTest` and `TestUtils`: clean up obsolete test cases and…
halotukozak 536abc5
Improve code formatting, standardize type annotations, and add langua…
halotukozak b78d233
Refactor analyzer rules and tests: improve type handling, simplify va…
halotukozak 22c852b
Refactor `BasePackage` analyzer: remove debug logs, simplify validati…
halotukozak File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,4 +29,5 @@ masterSlave/ | |
|
|
||
| .metals/ | ||
| .vscode/ | ||
| metals.sbt | ||
| metals.sbt | ||
| .planning/ | ||
129 changes: 54 additions & 75 deletions
129
analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,92 +1,71 @@ | ||
| package com.avsystem.commons | ||
| package analyzer | ||
|
|
||
| import scala.reflect.internal.util.NoPosition | ||
| import scala.tools.nsc.plugins.{Plugin, PluginComponent} | ||
| import scala.tools.nsc.{Global, Phase} | ||
| import dotty.tools.dotc.core.Contexts.Context | ||
| import dotty.tools.dotc.plugins.{PluginPhase, StandardPlugin} | ||
|
|
||
| final class AnalyzerPlugin(val global: Global) extends Plugin { plugin => | ||
| class AnalyzerPlugin extends StandardPlugin { | ||
|
|
||
| override def init(options: List[String], error: String => Unit): Boolean = { | ||
| options.foreach { option => | ||
| if (option.startsWith("requireJDK=")) { | ||
| val jdkVersionRegex = option.substring(option.indexOf('=') + 1) | ||
| override val description: String = "AVSystem custom Scala static analyzer" | ||
| val name: String = "AVSystemAnalyzer" | ||
| override def initialize(options: List[String])(using Context): List[PluginPhase] = { | ||
| val rules: List[AnalyzerRule] = List( | ||
| new ImportJavaUtil, | ||
| new VarargsAtLeast, | ||
| new CheckMacroPrivate, | ||
| new ExplicitGenerics, | ||
| new ValueEnumExhaustiveMatch, | ||
| new ShowAst, | ||
| new FindUsages, | ||
| new CheckBincompat, | ||
| new ThrowableObjects, | ||
| new DiscardedMonixTask, | ||
| new NothingAsFunctionArgument, | ||
| new ConstantDeclarations, | ||
| new BasePackage, | ||
| new ImplicitTypes, | ||
| new ImplicitValueClasses, | ||
| new FinalCaseClasses, | ||
| new ImplicitParamDefaults, | ||
| new CatchThrowable, | ||
| new ImplicitFunctionParams, | ||
| ) | ||
| parseOptions(options, rules) | ||
| rules.filter(_.level != Level.Off) | ||
| } | ||
|
|
||
| private def parseOptions(options: List[String], rules: List[AnalyzerRule])(using Context): Unit = { | ||
| val byName = rules.map(r => r.name -> r).toMap | ||
| options.foreach { opt => | ||
| if (opt.startsWith("requireJDK=")) { | ||
| val jdkVersionRegex = opt.substring(opt.indexOf('=') + 1) | ||
| val javaVersion = System.getProperty("java.version", "") | ||
| if (!javaVersion.matches(jdkVersionRegex)) { | ||
| global.reporter.error( | ||
| NoPosition, | ||
| s"This project must be compiled on JDK version that matches $jdkVersionRegex but got $javaVersion", | ||
| if (!javaVersion.matches(jdkVersionRegex)) | ||
| dotty.tools.dotc.report.error( | ||
| s"This project must be compiled on JDK version matching $jdkVersionRegex but got $javaVersion", | ||
| ) | ||
| } | ||
| } else { | ||
| val level = option.charAt(0) match { | ||
| case '-' => Level.Off | ||
| case '*' => Level.Info | ||
| case '+' => Level.Error | ||
| case _ => Level.Warn | ||
| val (level, nameArg) = opt.headOption match { | ||
| case Some('-') => (Level.Off, opt.drop(1)) | ||
| case Some('+') => (Level.Error, opt.drop(1)) | ||
| case Some('*') => (Level.Info, opt.drop(1)) | ||
| case _ => (Level.Warn, opt) | ||
| } | ||
| val nameArg = if (level != Level.Warn) option.drop(1) else option | ||
| if (nameArg == "_") { | ||
| // Split on ':' to extract per-rule argument, e.g. "findUsages:com.foo.Bar" | ||
| val colonIdx = nameArg.indexOf(':') | ||
| val (ruleName, ruleArg) = | ||
| if (colonIdx >= 0) (nameArg.substring(0, colonIdx), Some(nameArg.substring(colonIdx + 1))) | ||
| else (nameArg, None) | ||
| if (ruleName == "_") | ||
| rules.foreach(_.level = level) | ||
| } else { | ||
| val (name, arg) = nameArg.split(":", 2) match { | ||
| case Array(n, a) => (n, a) | ||
| case Array(n) => (n, null) | ||
| } | ||
| rulesByName.get(name) match { | ||
| else | ||
| byName.get(ruleName) match { | ||
| case Some(rule) => | ||
| rule.level = level | ||
| rule.argument = arg | ||
| case None => | ||
| error(s"Unrecognized AVS analyzer rule: $name") | ||
| ruleArg.foreach(arg => rule.argument = Some(arg)) | ||
| case None => dotty.tools.dotc.report.error(s"Unrecognized AVS analyzer rule: $name") | ||
| } | ||
| } | ||
| } | ||
| } | ||
| true | ||
| } | ||
|
|
||
| private lazy val rules = List( | ||
| new ImportJavaUtil(global), | ||
| new VarargsAtLeast(global), | ||
| new CheckMacroPrivate(global), | ||
| new ExplicitGenerics(global), | ||
| new ValueEnumExhaustiveMatch(global), | ||
| new ShowAst(global), | ||
| new FindUsages(global), | ||
| new CheckBincompat(global), | ||
| new Any2StringAdd(global), | ||
| new ThrowableObjects(global), | ||
| new DiscardedMonixTask(global), | ||
| new NothingAsFunctionArgument(global), | ||
| new ConstantDeclarations(global), | ||
| new BasePackage(global), | ||
| new ImplicitValueClasses(global), | ||
| new FinalValueClasses(global), | ||
| new FinalCaseClasses(global), | ||
| new ImplicitParamDefaults(global), | ||
| new CatchThrowable(global), | ||
| new ImplicitFunctionParams(global), | ||
| ) | ||
|
|
||
| private lazy val rulesByName = rules.map(r => (r.name, r)).toMap | ||
|
|
||
| val name = "AVSystemAnalyzer" | ||
| val description = "AVSystem custom Scala static analyzer" | ||
| val components: List[PluginComponent] = List(component) | ||
|
|
||
| private object component extends PluginComponent { | ||
| val global: plugin.global.type = plugin.global | ||
| val runsAfter: List[String] = List("typer") | ||
| override val runsBefore: List[String] = List("patmat") | ||
| val phaseName = "avsAnalyze" | ||
|
|
||
| import global._ | ||
|
|
||
| def newPhase(prev: Phase): StdPhase = new StdPhase(prev) { | ||
| def apply(unit: CompilationUnit): Unit = | ||
| rules.foreach(rule => if (rule.level != Level.Off) rule.analyze(unit.asInstanceOf[rule.global.CompilationUnit])) | ||
| } | ||
| } | ||
|
|
||
| } | ||
102 changes: 49 additions & 53 deletions
102
analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerRule.scala
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,59 +1,55 @@ | ||
| package com.avsystem.commons | ||
| package analyzer | ||
|
|
||
| import java.io.{PrintWriter, StringWriter} | ||
| import scala.tools.nsc.Global | ||
| import scala.tools.nsc.Reporting.WarningCategory | ||
| import scala.util.control.NonFatal | ||
|
|
||
| abstract class AnalyzerRule(val global: Global, val name: String, defaultLevel: Level = Level.Warn) { | ||
|
|
||
| import global._ | ||
|
|
||
| var level: Level = defaultLevel | ||
| var argument: String = _ | ||
|
|
||
| protected def classType(fullName: String): Type = | ||
| try rootMirror.staticClass(fullName).asType.toType.erasure | ||
| catch { | ||
| case _: ScalaReflectionException => NoType | ||
| } | ||
|
|
||
| protected def analyzeTree(fun: PartialFunction[Tree, Unit])(tree: Tree): Unit = | ||
| try fun.applyOrElse(tree, (_: Tree) => ()) | ||
| catch { | ||
| case NonFatal(t) => | ||
| val sw = new StringWriter | ||
| t.printStackTrace(new PrintWriter(sw)) | ||
| reporter.error(tree.pos, s"Analyzer rule $this failed: " + sw.toString) | ||
| } | ||
|
|
||
| private def adjustMsg(msg: String): String = s"[AVS] $msg" | ||
|
|
||
| protected final def report( | ||
| pos: Position, | ||
| message: String, | ||
| category: WarningCategory = WarningCategory.Lint, | ||
| site: Symbol = NoSymbol, | ||
| level: Level = this.level, | ||
| ): Unit = | ||
| level match { | ||
| case Level.Off => | ||
| case Level.Info => reporter.echo(pos, adjustMsg(message)) | ||
| case Level.Warn => currentRun.reporting.warning(pos, adjustMsg(message), category, site) | ||
| case Level.Error => reporter.error(pos, adjustMsg(message)) | ||
| } | ||
|
|
||
| def analyze(unit: CompilationUnit): Unit | ||
|
|
||
| override def toString: String = | ||
| getClass.getSimpleName | ||
| import dotty.tools.dotc.ast.tpd | ||
| import dotty.tools.dotc.core.Contexts.Context | ||
| import dotty.tools.dotc.plugins.PluginPhase | ||
| import dotty.tools.dotc.typer.TyperPhase | ||
| import dotty.tools.dotc.transform.Pickler | ||
|
|
||
| /** | ||
| * Base trait for all AVSystem analyzer rules. | ||
| * | ||
| * Each rule is a Scala 3 [[PluginPhase]] (MiniPhase) that runs after the typer | ||
| * and before the pickler. Rules override one or more `transformX` hooks from | ||
| * [[dotty.tools.dotc.transform.MegaPhase.MiniPhase]] to inspect the typed tree and | ||
| * emit diagnostics via the [[report]] helper. | ||
| * | ||
| * Rules MUST return the original tree unchanged from every `transformX` override | ||
| * (analysis-only; no tree modification). | ||
| */ | ||
| trait AnalyzerRule extends PluginPhase { | ||
|
|
||
| /** Short name used in plugin options, e.g. `catchThrowable` for `-P:AVSystemAnalyzer:+catchThrowable`. */ | ||
| val name: String | ||
|
|
||
| /** Current diagnostic level. Mutable so [[AnalyzerPlugin]] can configure it from plugin options. */ | ||
| var level: Level = Level.Warn | ||
|
|
||
| /** | ||
| * Optional per-rule argument, e.g. the class name for `findUsages:com.foo.Bar`. | ||
| * Populated by [[AnalyzerPlugin]] when parsing `ruleName:arg` option syntax. | ||
| */ | ||
| var argument: Option[String] = None | ||
|
|
||
| /** Unique phase name used by the Scala 3 phase scheduler. Namespaced to avoid conflicts. */ | ||
| final def phaseName: String = s"avs.$name" | ||
|
|
||
| /** Run after the typer phase so all symbols and types are fully resolved. */ | ||
| override def runsAfter: Set[String] = Set(TyperPhase.name) | ||
|
|
||
| /** Run before the pickler so we operate on the original typed tree (not yet serialised to TASTy). */ | ||
| override def runsBefore: Set[String] = Set(Pickler.name) | ||
|
|
||
| /** Emit a diagnostic at the given tree's source position, using the configured [[level]]. */ | ||
| protected final def report(tree: tpd.Tree, message: String)(using Context): Unit = level match { | ||
| case Level.Off => () | ||
| case Level.Info => dotty.tools.dotc.report.echo(s"[AVS] $message", tree.srcPos) | ||
| case Level.Warn => dotty.tools.dotc.report.warning(s"[AVS] $message", tree.srcPos) | ||
| case Level.Error => dotty.tools.dotc.report.error(s"[AVS] $message", tree.srcPos) | ||
| } | ||
| } | ||
|
|
||
| sealed trait Level | ||
| object Level { | ||
| case object Off extends Level | ||
| case object Info extends Level | ||
| case object Warn extends Level | ||
| case object Error extends Level | ||
| enum Level { | ||
| case Off, Info, Warn, Error | ||
| } |
22 changes: 0 additions & 22 deletions
22
analyzer/src/main/scala/com/avsystem/commons/analyzer/Any2StringAdd.scala
This file was deleted.
Oops, something went wrong.
45 changes: 26 additions & 19 deletions
45
analyzer/src/main/scala/com/avsystem/commons/analyzer/BasePackage.scala
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,29 +1,36 @@ | ||
| package com.avsystem.commons | ||
| package analyzer | ||
|
|
||
| import scala.annotation.tailrec | ||
| import scala.tools.nsc.Global | ||
| import dotty.tools.dotc.ast.tpd | ||
| import dotty.tools.dotc.core.Contexts.Context | ||
| import dotty.tools.dotc.core.Symbols | ||
| import dotty.tools.dotc.core.Symbols.Symbol | ||
|
|
||
| class BasePackage(g: Global) extends AnalyzerRule(g, "basePackage") { | ||
| import scala.annotation.tailrec | ||
|
|
||
| import global._ | ||
| class BasePackage(using Context) extends AnalyzerRule { | ||
| private lazy val requiredBasePackage = argument.map(Symbols.requiredPackage) | ||
|
|
||
| object SkipImports { | ||
| @tailrec def unapply(stats: List[Tree]): Some[List[Tree]] = stats match { | ||
| case Import(_, _) :: tail => unapply(tail) | ||
| case stats => Some(stats) | ||
| } | ||
| val name: String = "basePackage" | ||
| override def transformUnit(tree: tpd.Tree)(using Context): tpd.Tree = { | ||
| requiredBasePackage.foreach(validate(tree, _)) | ||
| tree | ||
| } | ||
|
|
||
| def analyze(unit: CompilationUnit): Unit = if (argument != null) { | ||
| val requiredBasePackage = argument | ||
|
|
||
| @tailrec def validate(tree: Tree): Unit = tree match { | ||
| case PackageDef(pid, _) if pid.symbol.hasPackageFlag && pid.symbol.fullName == requiredBasePackage => | ||
| case PackageDef(_, SkipImports(List(stat))) => validate(stat) | ||
| case t => report(t.pos, s"`$requiredBasePackage` must be one of the base packages in this file") | ||
| } | ||
|
|
||
| validate(unit.body) | ||
| @tailrec | ||
| private def validate(tree: tpd.Tree, required: Symbol)(using Context): Unit = tree match { | ||
| case pkg: tpd.PackageDef if pkg.pid.symbol == required => | ||
| // Found the required base package -- validation passes | ||
| () | ||
| case pkg: tpd.PackageDef => | ||
| // Skip imports, recurse into the single remaining stat (if exactly one) | ||
| val nonImports = pkg.stats.filterNot(_.isInstanceOf[tpd.Import]) | ||
| nonImports match { | ||
| case stat :: Nil => validate(stat, required) | ||
| case _ => | ||
| report(tree, s"`$required` must be one of the base packages in this file") | ||
| } | ||
| case _ => | ||
| report(tree, s"`$required` must be one of the base packages in this file") | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.