Skip to content
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

enh(swift) highlight function and macro call usage #3959

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGES.md
Expand Up @@ -27,6 +27,7 @@ Core Grammars:
- fix(swift) ensure keyword attributes highlight correctly [Bradley Mackey][]
- fix(types) fix interface LanguageDetail > keywords [Patrick Chiu]
- enh(java) add `goto` to be recognized as a keyword in Java [Alvin Joy][]
- enh(swift) highlight function and macro call usage [Bradley Mackey][]

New Grammars:

Expand Down
88 changes: 65 additions & 23 deletions src/languages/swift.js
Expand Up @@ -11,11 +11,17 @@ import * as Swift from './lib/kws_swift.js';
import {
concat,
either,
lookahead
lookahead,
negativeLookahead
} from '../lib/regex.js';

/** @type LanguageFn */
export default function(hljs) {
/**
* Regex for detecting a function call following an identifier.
*/
const TRAILING_PAREN_REGEX = /[^\S\r\n]*\(/;

const WHITESPACE = {
match: /\s+/,
relevance: 0
Expand All @@ -40,9 +46,9 @@ export default function(hljs) {
],
className: { 2: "keyword" }
};
const KEYWORD_GUARD = {
// Consume .keyword to prevent highlighting properties and methods as keywords.
match: concat(/\./, either(...Swift.keywords)),
const KEYWORD_PROP_GUARD = {
// Consume .keyword to prevent highlighting properties as keywords. .methods are highlighted seperately
match: concat(/\./, either(...Swift.keywords), negativeLookahead(TRAILING_PAREN_REGEX)),
relevance: 0
};
const PLAIN_KEYWORDS = Swift.keywords
Expand Down Expand Up @@ -70,24 +76,14 @@ export default function(hljs) {
};
const KEYWORD_MODES = [
DOT_KEYWORD,
KEYWORD_GUARD,
KEYWORD_PROP_GUARD,
KEYWORD
];

// https://github.com/apple/swift/tree/main/stdlib/public/core
const BUILT_IN_GUARD = {
// Consume .built_in to prevent highlighting properties and methods.
match: concat(/\./, either(...Swift.builtIns)),
relevance: 0
};
const BUILT_IN = {
className: 'built_in',
match: concat(/\b/, either(...Swift.builtIns), /(?=\()/)
scope: 'built_in',
match: concat(/\b/, either(...Swift.builtIns), lookahead(TRAILING_PAREN_REGEX)),
};
const BUILT_INS = [
BUILT_IN_GUARD,
BUILT_IN
];

// https://docs.swift.org/swift-book/ReferenceManual/LexicalStructure.html#ID418
const OPERATOR_GUARD = {
Expand Down Expand Up @@ -335,7 +331,7 @@ export default function(hljs) {
...COMMENTS,
REGEXP,
...KEYWORD_MODES,
...BUILT_INS,
BUILT_IN,
...OPERATORS,
NUMBER,
STRING,
Expand Down Expand Up @@ -390,13 +386,16 @@ export default function(hljs) {
endsParent: true,
illegal: /["']/
};

const FUNCTION_IDENT = either(QUOTED_IDENTIFIER.match, Swift.identifier, Swift.operator);

// https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#ID362
// https://docs.swift.org/swift-book/documentation/the-swift-programming-language/declarations/#Macro-Declaration
const FUNCTION_OR_MACRO = {
match: [
/(func|macro)/,
/\s+/,
either(QUOTED_IDENTIFIER.match, Swift.identifier, Swift.operator)
FUNCTION_IDENT,
],
className: {
1: "keyword",
Expand Down Expand Up @@ -491,14 +490,56 @@ export default function(hljs) {
]
};

function noneOf(list) {
return negativeLookahead(either(...list));
}

const METHODS_ONLY = [...Swift.keywords, ...Swift.builtIns];
const FUNCTION_CALL = {
relevance: 0,
variants: [
{
// Functions and macro calls
scope: "title.function",
keywords: KEYWORDS,
match: concat(
either(/\b/, /#/),
noneOf(METHODS_ONLY.map(x => concat(x, TRAILING_PAREN_REGEX))),
FUNCTION_IDENT,
lookahead(TRAILING_PAREN_REGEX),
),
},
{
// Keywords/built-ins that only can appear as a method call
// e.g. foo.if()
match: [
/\./,
either(...METHODS_ONLY),
TRAILING_PAREN_REGEX,
],
scope: {
2: "title.function",
}
},
{
// Quoted methods calls, e.g. `foo`()
scope: "title.function",
match: concat(
QUOTED_IDENTIFIER.match,
lookahead(TRAILING_PAREN_REGEX),
)
}
]
};

// Add supported submodes to string interpolation.
for (const variant of STRING.variants) {
const interpolation = variant.contains.find(mode => mode.label === "interpol");
// TODO: Interpolation can contain any expression, so there's room for improvement here.
interpolation.keywords = KEYWORDS;
const submodes = [
...KEYWORD_MODES,
...BUILT_INS,
BUILT_IN,
...OPERATORS,
NUMBER,
STRING,
Expand Down Expand Up @@ -535,14 +576,15 @@ export default function(hljs) {
},
REGEXP,
...KEYWORD_MODES,
...BUILT_INS,
BUILT_IN,
...OPERATORS,
NUMBER,
STRING,
...IDENTIFIERS,
...ATTRIBUTES,
TYPE,
TUPLE
TUPLE,
FUNCTION_CALL,
...IDENTIFIERS,
]
};
}
8 changes: 8 additions & 0 deletions src/lib/regex.js
Expand Up @@ -25,6 +25,14 @@ export function lookahead(re) {
return concat('(?=', re, ')');
}

/**
* @param {RegExp | string } re
* @returns {string}
*/
export function negativeLookahead(re) {
return concat('(?!', re, ')');
}

/**
* @param {RegExp | string } re
* @returns {string}
Expand Down
64 changes: 64 additions & 0 deletions test/markup/swift/functions.expect.txt
Expand Up @@ -17,6 +17,9 @@
<span class="hljs-params">p5</span>: <span class="hljs-meta">@attribute</span> <span class="hljs-type">String</span>? <span class="hljs-operator">=</span> <span class="hljs-string">&quot;text&quot;</span>
) { }

<span class="hljs-keyword">func</span> <span class="hljs-title function_">`escaped`</span>() {}
<span class="hljs-keyword">func</span> <span class="hljs-title function_">`if`</span>() {}

<span class="hljs-keyword">init</span>&lt;<span class="hljs-type">X</span>: <span class="hljs-type">A</span>&gt;(<span class="hljs-keyword">_</span> <span class="hljs-params">p</span>: <span class="hljs-meta">@attribute</span> <span class="hljs-keyword">inout</span> (x: <span class="hljs-type">Int</span>, var: <span class="hljs-type">Int</span>) <span class="hljs-operator">=</span> (<span class="hljs-number">0</span>, <span class="hljs-number">0</span>)) { }
<span class="hljs-keyword">init?</span>(<span class="hljs-keyword">_</span> <span class="hljs-params">p</span>: <span class="hljs-meta">@attribute</span> <span class="hljs-keyword">inout</span> (x: <span class="hljs-type">Int</span>, var: <span class="hljs-type">Int</span>) <span class="hljs-operator">=</span> (<span class="hljs-number">0</span>, <span class="hljs-number">0</span>)) { }
<span class="hljs-keyword">init!</span> (<span class="hljs-keyword">_</span> <span class="hljs-params">p</span>: <span class="hljs-meta">@attribute</span> <span class="hljs-keyword">inout</span> (x: <span class="hljs-type">Int</span>, var: <span class="hljs-type">Int</span>) <span class="hljs-operator">=</span> (<span class="hljs-number">0</span>, <span class="hljs-number">0</span>)) { }
Expand All @@ -30,3 +33,64 @@
<span class="hljs-keyword">static</span> <span class="hljs-keyword">func</span> <span class="hljs-title function_">&gt;</span> (<span class="hljs-params">lhs</span>: <span class="hljs-keyword">Self</span>, <span class="hljs-params">rhs</span>: <span class="hljs-keyword">Self</span>) -&gt; <span class="hljs-type">Bool</span>
<span class="hljs-keyword">static</span> <span class="hljs-keyword">func</span> <span class="hljs-title function_">&gt;=</span> (<span class="hljs-params">lhs</span>: <span class="hljs-keyword">Self</span>, <span class="hljs-params">rhs</span>: <span class="hljs-keyword">Self</span>) -&gt; <span class="hljs-type">Bool</span>
}

<span class="hljs-comment">// paren spacing</span>
obj.<span class="hljs-title function_">fn</span>(<span class="hljs-number">1</span>)
obj.<span class="hljs-title function_">fn</span> (<span class="hljs-number">1</span>)
obj.prop
(<span class="hljs-number">1</span>) <span class="hljs-comment">// newline break, this is no longer a function</span>

<span class="hljs-comment">// builtins</span>
<span class="hljs-built_in">abs</span>(<span class="hljs-number">1</span>)
<span class="hljs-built_in">swap</span>(<span class="hljs-operator">&amp;</span>a, <span class="hljs-operator">&amp;</span>b)
<span class="hljs-built_in">zip</span>(a, b)
obj.<span class="hljs-title function_">abs</span>(<span class="hljs-number">1</span>)
obj.<span class="hljs-title function_">swap</span>(<span class="hljs-operator">&amp;</span>a, <span class="hljs-operator">&amp;</span>b)
obj.<span class="hljs-title function_">zip</span>(a, b)
obj.<span class="hljs-title function_">abs</span> (<span class="hljs-number">1</span>)
obj.abs
(<span class="hljs-number">1</span>)

<span class="hljs-comment">// methods</span>
<span class="hljs-title function_">method</span>()
<span class="hljs-title function_">method</span>(<span class="hljs-number">1</span>)
<span class="hljs-title function_">method</span>(param: <span class="hljs-number">1</span>)
obj.<span class="hljs-title function_">method</span>()
obj .<span class="hljs-title function_">method</span>()
obj.<span class="hljs-title function_">method</span>(<span class="hljs-number">1</span>)
obj.<span class="hljs-title function_">method</span>(param: <span class="hljs-number">1</span>)
obj.prop.<span class="hljs-title function_">method</span>()
obj.prop .<span class="hljs-title function_">method</span>()
obj.prop.<span class="hljs-title function_">method</span>(<span class="hljs-number">1</span>)
obj.prop.<span class="hljs-title function_">method</span>(param: <span class="hljs-number">1</span>)
obj.prop.<span class="hljs-title function_">method</span>(
param: <span class="hljs-number">1</span>
)
obj.prop
.<span class="hljs-title function_">method</span>()

<span class="hljs-comment">// keywords</span>
obj.<span class="hljs-title function_">if</span>(condition: <span class="hljs-literal">true</span>)
obj.if <span class="hljs-comment">// variable</span>
obj .if <span class="hljs-comment">// variable</span>
<span class="hljs-title function_">`if`</span>()
obj.<span class="hljs-title function_">`if`</span>()
obj.<span class="hljs-title function_">`if`</span> ()
<span class="hljs-title function_">`notKeyword`</span>()
obj.<span class="hljs-title function_">`notKeyword`</span>()
obj.<span class="hljs-title function_">`notKeyword`</span> ()

<span class="hljs-comment">// number sign keywords are fine</span>
<span class="hljs-title function_">column</span>()
<span class="hljs-title function_">keyPath</span>()
<span class="hljs-title function_">sourceLocation</span>()
obj.<span class="hljs-title function_">column</span>()
obj.<span class="hljs-title function_">keyPath</span>()
obj.<span class="hljs-title function_">sourceLocation</span>()

<span class="hljs-comment">// attribute keywords are fine</span>
<span class="hljs-title function_">frozen</span>()
<span class="hljs-title function_">discardableResult</span>()
obj.<span class="hljs-title function_">frozen</span>()
obj.<span class="hljs-title function_">discardableResult</span>()

64 changes: 64 additions & 0 deletions test/markup/swift/functions.txt
Expand Up @@ -17,6 +17,9 @@ func f3(
p5: @attribute String? = "text"
) { }

func `escaped`() {}
func `if`() {}

init<X: A>(_ p: @attribute inout (x: Int, var: Int) = (0, 0)) { }
init?(_ p: @attribute inout (x: Int, var: Int) = (0, 0)) { }
init! (_ p: @attribute inout (x: Int, var: Int) = (0, 0)) { }
Expand All @@ -30,3 +33,64 @@ protocol Comparable: Equatable {
static func > (lhs: Self, rhs: Self) -> Bool
static func >= (lhs: Self, rhs: Self) -> Bool
}

// paren spacing
obj.fn(1)
obj.fn (1)
obj.prop
(1) // newline break, this is no longer a function

// builtins
abs(1)
swap(&a, &b)
zip(a, b)
obj.abs(1)
obj.swap(&a, &b)
obj.zip(a, b)
obj.abs (1)
obj.abs
(1)

// methods
method()
method(1)
method(param: 1)
obj.method()
obj .method()
obj.method(1)
obj.method(param: 1)
obj.prop.method()
obj.prop .method()
obj.prop.method(1)
obj.prop.method(param: 1)
obj.prop.method(
param: 1
)
obj.prop
.method()

// keywords
obj.if(condition: true)
obj.if // variable
obj .if // variable
`if`()
obj.`if`()
obj.`if` ()
`notKeyword`()
obj.`notKeyword`()
obj.`notKeyword` ()

// number sign keywords are fine
column()
keyPath()
sourceLocation()
obj.column()
obj.keyPath()
obj.sourceLocation()

// attribute keywords are fine
frozen()
discardableResult()
obj.frozen()
obj.discardableResult()

21 changes: 17 additions & 4 deletions test/markup/swift/keywords.expect.txt
Expand Up @@ -19,10 +19,23 @@ x <span class="hljs-keyword">is</span> <span class="hljs-type">String</span>
<span class="hljs-keyword">isolated</span> <span class="hljs-keyword">nonisolated</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">fileprivate</span> <span class="hljs-keyword">package</span> <span class="hljs-keyword">internal</span> <span class="hljs-keyword">open</span>

<span class="hljs-keyword">#if</span>
<span class="hljs-keyword">#error</span>(<span class="hljs-string">&quot;Error&quot;</span>)
<span class="hljs-keyword">#if</span> <span class="hljs-type">DEBUG</span>
<span class="hljs-title function_"><span class="hljs-keyword">#error</span></span>(<span class="hljs-string">&quot;Error&quot;</span>)
<span class="hljs-keyword">#elseif</span> <span class="hljs-title function_">os</span>(macOS)
<span class="hljs-title function_"><span class="hljs-keyword">#error</span></span>(<span class="hljs-string">&quot;Error&quot;</span>)
<span class="hljs-keyword">#elseif</span> <span class="hljs-title function_">arch</span>(arm64)
<span class="hljs-title function_"><span class="hljs-keyword">#error</span></span>(<span class="hljs-string">&quot;Error&quot;</span>)
<span class="hljs-keyword">#elseif</span> <span class="hljs-title function_">compiler</span>(<span class="hljs-operator">&gt;=</span><span class="hljs-number">5.0</span>)
<span class="hljs-title function_"><span class="hljs-keyword">#error</span></span>(<span class="hljs-string">&quot;Error&quot;</span>)
<span class="hljs-keyword">#elseif</span> <span class="hljs-title function_">canImport</span>(<span class="hljs-type">UIKit</span>)
<span class="hljs-title function_"><span class="hljs-keyword">#error</span></span>(<span class="hljs-string">&quot;Error&quot;</span>)
<span class="hljs-keyword">#elseif</span> <span class="hljs-title function_">targetEnvironment</span>(simulator)
<span class="hljs-title function_"><span class="hljs-keyword">#error</span></span>(<span class="hljs-string">&quot;Error&quot;</span>)
<span class="hljs-keyword">#endif</span>

x.as(y)
x.for(y)
<span class="hljs-title function_"><span class="hljs-keyword">#imageLiteral</span></span>(resourceName: expression)
<span class="hljs-title function_"><span class="hljs-keyword">#fileLiteral</span></span>(resourceName: expression)

x.<span class="hljs-title function_">as</span>(y)
x.<span class="hljs-title function_">for</span>(y)
#notAKeyword
15 changes: 14 additions & 1 deletion test/markup/swift/keywords.txt
Expand Up @@ -19,10 +19,23 @@ async await
isolated nonisolated
public private fileprivate package internal open

#if
#if DEBUG
#error("Error")
#elseif os(macOS)
#error("Error")
#elseif arch(arm64)
#error("Error")
#elseif compiler(>=5.0)
#error("Error")
#elseif canImport(UIKit)
#error("Error")
#elseif targetEnvironment(simulator)
#error("Error")
#endif

#imageLiteral(resourceName: expression)
#fileLiteral(resourceName: expression)

x.as(y)
x.for(y)
#notAKeyword