Skip to content

Add diagnostic for canImport expression #1574

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

Merged
merged 5 commits into from
May 8, 2023
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
57 changes: 33 additions & 24 deletions CodeGeneration/Sources/SyntaxSupport/AvailabilityNodes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,42 +118,51 @@ public let AVAILABILITY_NODES: [Node] = [
]
),

// version-tuple -> integer-literal
// | float-literal
// | float-literal '.' integer-literal
// version-tuple-element -> '.' integer-literal
Node(
name: "VersionTuple",
nameForDiagnostics: "version tuple",
description: "A version number of the form major.minor.patch in which the minor and patch part may be omitted.",
name: "VersionComponent",
nameForDiagnostics: nil,
description: "An element to represent a single component in a version, like `.1`.",
kind: "Syntax",
children: [
Child(
name: "Major",
kind: .token(choices: [.token(tokenKind: "IntegerLiteralToken")]),
description: "The major version."
),
Child(
name: "MinorPeriod",
name: "Period",
kind: .token(choices: [.token(tokenKind: "PeriodToken")]),
description: "If the version contains a minor number, the period separating the major from the minor number.",
isOptional: true
description: "The period of this version component"
),
Child(
name: "Minor",
name: "Number",
kind: .token(choices: [.token(tokenKind: "IntegerLiteralToken")]),
description: "The minor version if specified.",
isOptional: true
description: "The version number of this component"
),
]
),

// version-list -> version-tuple-element version-list?
Node(
name: "VersionComponentList",
nameForDiagnostics: nil,
kind: "SyntaxCollection",
element: "VersionComponent",
omitWhenEmpty: true
),

// version-tuple -> integer-literal version-list?
Node(
name: "VersionTuple",
nameForDiagnostics: "version tuple",
description: "A version number like `1.2.0`. Only the first version component is required. There might be an arbitrary number of following components.",
kind: "Syntax",
children: [
Child(
name: "PatchPeriod",
kind: .token(choices: [.token(tokenKind: "PeriodToken")]),
description: "If the version contains a patch number, the period separating the minor from the patch number.",
isOptional: true
name: "Major",
kind: .token(choices: [.token(tokenKind: "IntegerLiteralToken")]),
description: "The major version."
),
Child(
name: "Patch",
kind: .token(choices: [.token(tokenKind: "IntegerLiteralToken")]),
description: "The patch version if specified.",
name: "Components",
kind: .collection(kind: "VersionComponentList", collectionElementName: "VersionComponent"),
description: "Any version components that are not the major version . For example, for `1.2.0`, this will contain `.2.0`",
isOptional: true
),
]
Expand Down
3 changes: 3 additions & 0 deletions CodeGeneration/Sources/SyntaxSupport/Child.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ public class Child {
isIndented: Bool = false,
requiresLeadingNewline: Bool = false
) {
if let firstCharInName = name.first {
precondition(firstCharInName.isUppercase == true, "The first letter of a child’s name should be uppercase")
}
self.name = name
self.kind = kind
self.nameForDiagnostics = nameForDiagnostics
Expand Down
54 changes: 54 additions & 0 deletions CodeGeneration/Sources/SyntaxSupport/ExprNodes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,60 @@ public let EXPR_NODES: [Node] = [
]
),

// the canImport expr in if config expression
Node(
name: "CanImportExpr",
nameForDiagnostics: "'canImport' expression",
kind: "Expr",
children: [
Child(
name: "CanImportKeyword",
kind: .token(choices: [.keyword(text: "canImport")])
),
Child(
name: "LeftParen",
kind: .token(choices: [.token(tokenKind: "LeftParenToken")])
),
Child(
name: "ImportPath",
kind: .token(choices: [.token(tokenKind: "IdentifierToken")])
),
Child(
name: "VersionInfo",
kind: .node(kind: "CanImportVersionInfo"),
isOptional: true
),
Child(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than have a string of optional components, can we split the version information into its own syntax element and make that optional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense to me. I've extracted the optional part of canImport to a separate node.

name: "RightParen",
kind: .token(choices: [.token(tokenKind: "RightParenToken")])
),
]
),

Node(
name: "CanImportVersionInfo",
nameForDiagnostics: nil,
kind: "Expr",
children: [
Child(
name: "Comma",
kind: .token(choices: [.token(tokenKind: "CommaToken")])
),
Child(
name: "Label",
kind: .token(choices: [.keyword(text: "_version"), .keyword(text: "_underlyingVersion")])
),
Child(
name: "Colon",
kind: .token(choices: [.token(tokenKind: "ColonToken")])
),
Child(
name: "VersionTuple",
kind: .node(kind: "VersionTuple")
),
]
),

// case-item -> pattern where-clause? ','?
Node(
name: "CaseItem",
Expand Down
4 changes: 4 additions & 0 deletions CodeGeneration/Sources/SyntaxSupport/KeywordSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ public let KEYWORDS: [KeywordSpec] = [
KeywordSpec("_TrivialAtMost"),
KeywordSpec("_typeEraser"),
KeywordSpec("_unavailableFromAsync"),
KeywordSpec("_underlyingVersion"),
KeywordSpec("_UnknownLayout"),
KeywordSpec("_version"),
KeywordSpec("actor"),
KeywordSpec("addressWithNativeOwner"),
KeywordSpec("addressWithOwner"),
Expand All @@ -102,9 +104,11 @@ public let KEYWORDS: [KeywordSpec] = [
KeywordSpec("block"),
KeywordSpec("borrowing"),
KeywordSpec("break", isLexerClassified: true, requiresTrailingSpace: true),
KeywordSpec("canImport"),
KeywordSpec("case", isLexerClassified: true, requiresTrailingSpace: true),
KeywordSpec("catch", isLexerClassified: true, requiresLeadingSpace: true),
KeywordSpec("class", isLexerClassified: true, requiresTrailingSpace: true),
KeywordSpec("compiler"),
KeywordSpec("consume"),
KeywordSpec("consuming"),
KeywordSpec("continue", isLexerClassified: true, requiresTrailingSpace: true),
Expand Down
68 changes: 34 additions & 34 deletions Sources/SwiftParser/Availability.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ extension Parser {
(.obsoleted, let handle)?:
let argumentLabel = self.eat(handle)
let (unexpectedBeforeColon, colon) = self.expect(.colon)
let version = self.parseVersionTuple()
let version = self.parseVersionTuple(maxComponentCount: 3)
entry = .availabilityLabeledArgument(
RawAvailabilityLabeledArgumentSyntax(
label: argumentLabel,
Expand All @@ -136,7 +136,7 @@ extension Parser {
case (.deprecated, let handle)?:
let argumentLabel = self.eat(handle)
if let colon = self.consume(if: .colon) {
let version = self.parseVersionTuple()
let version = self.parseVersionTuple(maxComponentCount: 3)
entry = .availabilityLabeledArgument(
RawAvailabilityLabeledArgumentSyntax(
label: argumentLabel,
Expand Down Expand Up @@ -227,7 +227,7 @@ extension Parser {

let version: RawVersionTupleSyntax?
if self.at(.integerLiteral, .floatingLiteral) {
version = self.parseVersionTuple()
version = self.parseVersionTuple(maxComponentCount: 3)
} else {
version = nil
}
Expand Down Expand Up @@ -258,47 +258,47 @@ extension Parser {
/// Grammar
/// =======
///
/// platform-version → decimal-digits
/// platform-version → decimal-digits '.' decimal-digits
/// platform-version → decimal-digits '.' decimal-digits '.' decimal-digits
mutating func parseVersionTuple() -> RawVersionTupleSyntax {
/// version-tuple -> integer-literal version-list?
/// version-list -> version-tuple-element version-list?
/// version-tuple-element -> '.' interger-literal
mutating func parseVersionTuple(maxComponentCount: Int) -> RawVersionTupleSyntax {
if self.at(.floatingLiteral),
let periodIndex = self.currentToken.tokenText.firstIndex(of: UInt8(ascii: ".")),
self.currentToken.tokenText[0..<periodIndex].allSatisfy({ Unicode.Scalar($0).isDigit })
{
// The lexer generates a float literal '1.2' for the major and minor version.
// Split it into two integers if possible
let major = self.consumePrefix(SyntaxText(rebasing: self.currentToken.tokenText[0..<periodIndex]), as: .integerLiteral)
let (unexpectedBeforeMinorPeriod, minorPeriod) = self.expect(.period)
let minor = self.expectDecimalIntegerWithoutRecovery()
let patchPeriod: RawTokenSyntax?
let patch: RawTokenSyntax?
if let period = self.consume(if: .period) {
patchPeriod = period
patch = self.expectDecimalIntegerWithoutRecovery()
} else {
patchPeriod = nil
patch = nil

var components: [RawVersionComponentSyntax] = []
var trailingComponents: [RawVersionComponentSyntax] = []

for i in 1... {
guard let period = self.consume(if: .period) else {
break
}
let version = self.expectDecimalIntegerWithoutRecovery()

let versionComponent = RawVersionComponentSyntax(period: period, number: version, arena: self.arena)

if i < maxComponentCount {
components.append(versionComponent)
} else {
trailingComponents.append(versionComponent)
}
}
return RawVersionTupleSyntax(
major: major,
unexpectedBeforeMinorPeriod,
minorPeriod: minorPeriod,
minor: minor,
patchPeriod: patchPeriod,
patch: patch,
arena: self.arena
)

var unexpectedTrailingComponents: RawUnexpectedNodesSyntax?

if !trailingComponents.isEmpty {
unexpectedTrailingComponents = RawUnexpectedNodesSyntax(elements: trailingComponents.compactMap { $0.as(RawSyntax.self) }, arena: self.arena)
}

return RawVersionTupleSyntax(major: major, components: RawVersionComponentListSyntax(elements: components, arena: self.arena), unexpectedTrailingComponents, arena: self.arena)

} else {
let major = self.expectDecimalIntegerWithoutRecovery()
return RawVersionTupleSyntax(
major: major,
minorPeriod: nil,
minor: nil,
patchPeriod: nil,
patch: nil,
arena: self.arena
)
return RawVersionTupleSyntax(major: major, components: nil, arena: self.arena)
}
}
}
59 changes: 58 additions & 1 deletion Sources/SwiftParser/Expressions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ extension Parser {
forDirective: Bool,
pattern: PatternContext
) -> RawExprSyntax {
let head = self.parsePrimaryExpression(pattern: pattern, flavor: flavor)
let head = self.parsePrimaryExpression(pattern: pattern, forDirective: forDirective, flavor: flavor)
guard !head.is(RawMissingExprSyntax.self) else {
return head
}
Expand Down Expand Up @@ -1151,8 +1151,15 @@ extension Parser {
@_spi(RawSyntax)
public mutating func parsePrimaryExpression(
pattern: PatternContext,
forDirective: Bool,
flavor: ExprFlavor
) -> RawExprSyntax {
if forDirective == true,
let directiveExpr = self.parsePrimaryExprForDirective()
{
return RawExprSyntax(directiveExpr)
}

switch self.at(anyIn: PrimaryExpressionStart.self) {
case (.integerLiteral, let handle)?:
let digits = self.eat(handle)
Expand Down Expand Up @@ -1316,6 +1323,18 @@ extension Parser {
return RawExprSyntax(RawMissingExprSyntax(arena: self.arena))
}
}

// try to parse a primary expression for a directive
mutating func parsePrimaryExprForDirective() -> RawExprSyntax? {
switch self.at(anyIn: CompilationCondition.self) {
case (.canImportKeyword, let handle)?:
return RawExprSyntax(self.parseCanImportExpression(handle))

// TODO: add case `swift` and `compiler` here
default:
return nil
}
}
}

extension Parser {
Expand Down Expand Up @@ -2569,6 +2588,44 @@ extension Parser {
}
}

// MARK: Platform Condition
extension Parser {
mutating func parseCanImportExpression(_ handle: TokenConsumptionHandle) -> RawExprSyntax {
let canImportKeyword = self.eat(handle)

let (unexpectedBeforeLeftParen, leftParen) = self.expect(.leftParen)

let (unexpectedBeforeImportPath, importPath) = self.expect(.identifier)

var versionInfo: RawCanImportVersionInfoSyntax?

if let comma = self.consume(if: .comma) {
let (unexpectedBeforeLabel, label) = self.expect(.keyword(._version), .keyword(._underlyingVersion), default: .keyword(._version))
let (unexpectedBeforeColon, colon) = self.expect(.colon)

let version = self.parseVersionTuple(maxComponentCount: 4)

versionInfo = RawCanImportVersionInfoSyntax(comma: comma, unexpectedBeforeLabel, label: label, unexpectedBeforeColon, colon: colon, versionTuple: version, arena: self.arena)
}

let (unexpectedBeforeRightParen, rightParen) = self.expect(.rightParen)

return RawExprSyntax(
RawCanImportExprSyntax(
canImportKeyword: canImportKeyword,
unexpectedBeforeLeftParen,
leftParen: leftParen,
unexpectedBeforeImportPath,
importPath: importPath,
versionInfo: versionInfo,
unexpectedBeforeRightParen,
rightParen: rightParen,
arena: self.arena
)
)
}
}

// MARK: Lookahead

extension Parser.Lookahead {
Expand Down
24 changes: 24 additions & 0 deletions Sources/SwiftParser/TokenSpecSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,30 @@ enum CanBeStatementStart: TokenSpecSet {
}
}

enum CompilationCondition: TokenSpecSet {
case swiftKeyword
case compilerKeyword
case canImportKeyword

init?(lexeme: Lexer.Lexeme) {
switch PrepareForKeywordMatch(lexeme) {
case TokenSpec(.swift): self = .swiftKeyword
case TokenSpec(.compiler): self = .compilerKeyword
case TokenSpec(.canImport): self = .canImportKeyword
default: return nil
}
}

var spec: TokenSpec {
switch self {
case .swiftKeyword: return .keyword(.swift)
case .compilerKeyword: return .keyword(.compiler)
case .canImportKeyword: return .keyword(.canImport)
}
}

}

enum ContextualDeclKeyword: TokenSpecSet {
case __consuming
case _compilerInitialized
Expand Down
Loading