Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 30 additions & 1 deletion doc/UNT0043.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Mistyped conditional compilation symbols in `#if` and `#elif` directives are tre
#endif
```

If the project defines `UNITY_STANDALONE_OSX`, `UNITTY_STANDALONE_OSX` is reported as very close to that project-level preprocessor symbol. The analyzer gets this symbol set from Roslyn's parse options for the project.
If the project defines `UNITY_STANDALONE_OSX` and no other symbol is an equally plausible correction, `UNITTY_STANDALONE_OSX` is reported as a likely typo. The analyzer gets the currently enabled symbols from Roslyn's parse options for the project; it does not maintain a catalog of Unity platform symbols.

The same applies to custom project symbols:

Expand All @@ -22,6 +22,35 @@ The same applies to custom project symbols:

If the project defines `FEATURE_RELEASE`, this is reported as a likely typo.

## Avoiding false positives

An undefined symbol can be intentional: its code may target another platform, Unity version, or optional feature. The analyzer only reports an undefined symbol when exactly one enabled project symbol differs by a single insertion, deletion, substitution, or adjacent transposition. Symbols shorter than three characters and symbols declared with a local `#define` are not reported.

Numeric components must remain unchanged. For example, `FEATURE_1` is not corrected to `FEATURE_2` or `FEATURE_12`. Splitting or merging numeric components is also excluded.

Well-formed Unity version symbols are never reported, regardless of whether they are enabled in the current project:

- `UNITY_X`
- `UNITY_X_Y`
- `UNITY_X_Y_Z`
- `UNITY_X_Y_OR_NEWER`

Here, `X`, `Y`, and `Z` are numeric components. For example, these conditions are intentional even in a Unity 6.3 project targeting Windows:

```csharp
#if UNITY_6000_5_OR_NEWER
UseNewerUnityApi();
#endif

#if UNITY_PS4 || UNITY_PS5
UsePlayStationFeatures();
#endif
```

The analyzer does not change version numbers or suggest `UNITY_64` for `UNITY_PS4`. Spelling errors such as `UNITY_EDTIOR` or `UNITY_6000_3_OR_NEWRE` can still be reported if the intended symbol is enabled and the correction is unambiguous.

This is a conservative heuristic, not validation of all possible symbols. It can miss typos for inactive platforms or optional features, and similarly named optional symbols can still produce false positives. Review the intended condition before applying a fix.

## Solution

Use the intended symbol name:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ void Method()
.WithArguments("UNITTY_STANDALONE_OSX", "UNITY_STANDALONE_OSX");

var context = AnalyzerVerificationContext.Default
.WithPreprocessorSymbols("UNITY_EDITOR", "UNITY_STANDALONE_WIN", "UNITY_STANDALONE_LINUX", "UNITY_STANDALONE_OSX");
.WithPreprocessorSymbols("UNITY_EDITOR", "UNITY_EDITOR_OSX", "UNITY_STANDALONE", "UNITY_STANDALONE_OSX", "UNITY_64");

await VerifyCSharpDiagnosticAsync(context, test, diagnostic);

Expand Down Expand Up @@ -115,29 +115,187 @@ void Method()
await VerifyCSharpDiagnosticAsync(context, test, diagnostic);
}

[Theory]
[InlineData("EATURE_RELEASE", "FEATURE_RELEASE")]
[InlineData("FEATURE_RELESE", "FEATURE_RELEASE")]
[InlineData("FEATURE_RELEAS", "FEATURE_RELEASE")]
[InlineData("FFEATURE_RELEASE", "FEATURE_RELEASE")]
[InlineData("FEATTURE_RELEASE", "FEATURE_RELEASE")]
[InlineData("FEATURE_RELEASEE", "FEATURE_RELEASE")]
[InlineData("GEATURE_RELEASE", "FEATURE_RELEASE")]
[InlineData("FEATURE_RELEAZE", "FEATURE_RELEASE")]
[InlineData("FEATURE_RELEASX", "FEATURE_RELEASE")]
[InlineData("EFATURE_RELEASE", "FEATURE_RELEASE")]
[InlineData("FETAURE_RELEASE", "FEATURE_RELEASE")]
[InlineData("FEATURE_RELEAES", "FEATURE_RELEASE")]
[InlineData("UNITY_EDTIOR", "UNITY_EDITOR")]
[InlineData("UNITY_6000_3_OR_NEWRE", "UNITY_6000_3_OR_NEWER")]
[InlineData("UNITY_6000__3_OR_NEWER", "UNITY_6000_3_OR_NEWER")]
[InlineData("UNITY_6000_3_OR_NEWE", "UNITY_6000_3_OR_NEWER")]
[InlineData("UNITY_6000_3_OR_NEWER_", "UNITY_6000_3_OR_NEWER")]
[InlineData("FEATURE_12_V2_RELEAS", "FEATURE_12_V2_RELEASE")]
[InlineData("FEATURE_1A2", "FEATURE_1B2")]
[InlineData("ACB", "ABC")]
public async Task SingleTypo(string symbol, string definedSymbol)
{
var context = AnalyzerVerificationContext.Default
.WithPreprocessorSymbols(definedSymbol);

var diagnostic = ExpectDiagnostic()
.WithLocation(6, 5)
.WithArguments(symbol, definedSymbol);

await VerifyCSharpDiagnosticAsync(context, Source(symbol), diagnostic);
await VerifyCSharpFixAsync(context, Source(symbol), Source(definedSymbol));
}

[Theory]
[InlineData("FEATURE_RELXXSE")]
[InlineData("FFEATURE_RELEASEE")]
[InlineData("FEATURE_RELEASXX")]
[InlineData("FETAURE_RELEAS")]
public async Task MultipleEdits(string symbol)
{
var context = AnalyzerVerificationContext.Default
.WithPreprocessorSymbols("FEATURE_RELEASE");

await VerifyCSharpDiagnosticAsync(context, Source(symbol));
}

[Theory]
[InlineData("FEATURE_1", "FEATURE_2")]
[InlineData("FEATURE_1", "FEATURE_12")]
[InlineData("FEATURE_12", "FEATURE_1")]
[InlineData("FEATURE_12", "FEATURE_21")]
[InlineData("FEATURE_01", "FEATURE_1")]
[InlineData("FEATURE", "FEATURE1")]
[InlineData("FEATURE1", "FEATURE")]
[InlineData("FEATURE_1_2", "FEATURE_12")]
[InlineData("FEATURE_12", "FEATURE_1_2")]
[InlineData("FEATURE_1A2", "FEATURE_12")]
[InlineData("FEATURE_V2_X3", "FEATURE_V2_X4")]
[InlineData("FEATURE_V2_X3", "FEATURE_V2_X")]
[InlineData("FEATURE_V2_X", "FEATURE_V2_X3")]
public async Task DifferentNumericComponents(string symbol, string definedSymbol)
{
var context = AnalyzerVerificationContext.Default
.WithPreprocessorSymbols(definedSymbol);

await VerifyCSharpDiagnosticAsync(context, Source(symbol));
}

[Theory]
[InlineData("UNITY_5")]
[InlineData("UNITY_2019_4")]
[InlineData("UNITY_2019_4_40")]
[InlineData("UNITY_6000_2")]
[InlineData("UNITY_6000_3_8")]
[InlineData("UNITY_6000_4_OR_NEWER")]
[InlineData("UNITY_6000_5_OR_NEWER")]
[InlineData("UNITY_9999_9_OR_NEWER")]
public async Task InactiveUnityVersionSymbol(string symbol)
{
var context = AnalyzerVerificationContext.Default
.WithPreprocessorSymbols("UNITY_6000", "UNITY_6000_3", "UNITY_6000_3_9",
"UNITY_6000_0_OR_NEWER", "UNITY_6000_1_OR_NEWER", "UNITY_6000_2_OR_NEWER", "UNITY_6000_3_OR_NEWER");

await VerifyCSharpDiagnosticAsync(context, Source(symbol));
}

[Theory]
[InlineData("UNITY_6000")]
[InlineData("UNITY_6000_4")]
[InlineData("UNITY_6000_4_1")]
[InlineData("UNITY_6000_4_OR_NEWER")]
public async Task UnityVersionSymbolResemblingCustomSymbol(string symbol)
{
var context = AnalyzerVerificationContext.Default
.WithPreprocessorSymbols(symbol + "_");

await VerifyCSharpDiagnosticAsync(context, Source(symbol));
}

[Theory]
[InlineData("UNITY_PS4 || UNITY_PS5")]
[InlineData("UNITY_STANDALONE_OSX || UNITY_STANDALONE_LINUX")]
[InlineData("UNITY_EDITOR || UNITY_STANDALONE || UNITY_PS4 || UNITY_PS5 || UNITY_WSA || UNITY_ANDROID || UNITY_IOS || UNITY_TVOS || UNITY_VISIONOS")]
public async Task InactivePlatformSymbols(string condition)
{
var context = AnalyzerVerificationContext.Default
.WithPreprocessorSymbols("UNITY_EDITOR", "UNITY_EDITOR_WIN", "UNITY_STANDALONE", "UNITY_STANDALONE_WIN", "UNITY_64");

await VerifyCSharpDiagnosticAsync(context, Source(condition));
}

[Fact]
public async Task ProjectSymbolTypo()
public async Task InactiveVersionSymbolsInElif()
{
const string test = @"
class Test
{
void Method()
{
#if FEATURE_RELEAS
#if UNITY_PS4 || UNITY_PS5
return;
#elif !UNITY_6000_4_OR_NEWER && !UNITY_6000_5_OR_NEWER
return;
#endif
}
}
";

var context = AnalyzerVerificationContext.Default
.WithPreprocessorSymbols("FEATURE_RELEASE");
.WithPreprocessorSymbols("UNITY_STANDALONE", "UNITY_STANDALONE_WIN", "UNITY_64", "UNITY_6000_3_OR_NEWER");

await VerifyCSharpDiagnosticAsync(context, test);
}

[Theory]
[InlineData("FEATURE_RELEASX", "FEATURE_RELEASE", "FEATURE_RELEASY")]
[InlineData("FEATURE_RELEAS", "FEATURE_RELEASE", "FEATURE_RELEAZ")]
[InlineData("UNITY_EDTIOR", "UNITY_EDITOR", "UNITY_EDTIOS")]
public async Task AmbiguousSymbol(string symbol, string firstCandidate, string secondCandidate)
{
var context = AnalyzerVerificationContext.Default
.WithPreprocessorSymbols(firstCandidate, secondCandidate);

await VerifyCSharpDiagnosticAsync(context, Source(symbol));

context = AnalyzerVerificationContext.Default
.WithPreprocessorSymbols(secondCandidate, firstCandidate);

await VerifyCSharpDiagnosticAsync(context, Source(symbol));
}

[Fact]
public async Task DuplicateDefinedSymbolsAreNotAmbiguous()
{
var context = AnalyzerVerificationContext.Default
.WithPreprocessorSymbols("FEATURE_RELEASE", "FEATURE_RELEASE");

var diagnostic = ExpectDiagnostic()
.WithLocation(6, 5)
.WithArguments("FEATURE_RELEAS", "FEATURE_RELEASE");

await VerifyCSharpDiagnosticAsync(context, test, diagnostic);
await VerifyCSharpDiagnosticAsync(context, Source("FEATURE_RELEAS"), diagnostic);
}

[Fact]
public async Task TypoForInactivePlatform()
{
var context = AnalyzerVerificationContext.Default
.WithPreprocessorSymbols("UNITY_EDITOR", "UNITY_EDITOR_WIN", "UNITY_STANDALONE", "UNITY_STANDALONE_WIN", "UNITY_64");

await VerifyCSharpDiagnosticAsync(context, Source("UNITTY_STANDALONE_OSX"));
}

[Fact]
public async Task ShortSymbol()
{
var context = AnalyzerVerificationContext.Default
.WithPreprocessorSymbols("ABC");

await VerifyCSharpDiagnosticAsync(context, Source("AB"));
}

[Fact]
Expand Down Expand Up @@ -199,7 +357,24 @@ void Method()
}
";

await VerifyCSharpDiagnosticAsync(test);
var context = AnalyzerVerificationContext.Default
.WithPreprocessorSymbols("UNITY_STANDALONE_OSX");

await VerifyCSharpDiagnosticAsync(context, test);
}

private static string Source(string condition)
{
return $@"
class Test
{{
void Method()
{{
#if {condition}
return;
#endif
}}
}}
";
}
}
Loading
Loading