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

(swift) Revamped keywords and added support for operators #2908

Merged
merged 20 commits into from
Dec 15, 2020
Merged
Show file tree
Hide file tree
Changes from 12 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
7 changes: 6 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ New Languages:
Language improvements:

- enh(makefile): Add `make` as an alias (#2883) [tripleee][]
- enh(swift) Improved grammar for strings (#2819) [Steven Van Impe][]
- enh(swift) Grammar improvements (#2908) [Steven Van Impe][]
- New grammar for keywords and built-ins
- Added support for operator highlighting
- New grammar for attributes
- Added support for quoted identifiers, implicit parameters, and property wrapper projections
- Support for more complex expressions in string interpolation
- fix(asciidoc): Handle section titles level 5 (#2868) [Vaibhav Chanana][]

Grammar improvements:
Expand Down
301 changes: 301 additions & 0 deletions src/languages/lib/swift.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
/* eslint-disable no-misleading-character-class */
Copy link
Member

Choose a reason for hiding this comment

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

Please don't add these, if there are issues remaining we need to resolve them.

import {
concat,
either
} from '../../lib/regex.js';

const keywordWrapper = keyword => concat(
/\b/,
keyword,
/\w$/.test(keyword) ? /\b/ : /\B/
);

// Keywords that require a leading dot.
export const dotKeywords = [
'Protocol', // contextual
'Type' // contextual
].map(keywordWrapper);

// Keywords that may have a leading dot.
export const optionalDotKeywords = [
'init',
'self'
].map(keywordWrapper);

// Regular keywords and literals.
export const keywords = [
'Any',
'Self',
'associatedtype',
'as\\?', // operator
'as!', // operator
'as', // operator
'break',
'case',
'catch',
'class',
'continue',
'convenience', // contextual
'default',
'defer',
'deinit',
'didSet', // contextual
'do',
'dynamic', // contextual
'else',
'enum',
'extension',
'fallthrough',
'fileprivate\\(set\\)',
'fileprivate',
'final', // contextual
'for',
'func',
'get', // contextual
'guard',
'if',
'import',
'indirect', // contextual
'infix', // contextual
'init\\?',
'init!',
'inout',
'internal\\(set\\)',
'internal',
'in',
'is', // operator
'lazy', // contextual
'let',
'mutating', // contextual
'nonmutating', // contextual
'open\\(set\\)', // contextual
'open', // contextual
'operator',
'optional', // contextual
'override', // contextual
'postfix', // contextual
'precedencegroup',
'prefix', // contextual
'private\\(set\\)',
'private',
'protocol',
'public\\(set\\)',
'public',
'repeat',
'required', // contextual
'rethrows',
'return',
'set', // contextual
'some', // contextual
'static',
'struct',
'subscript',
'super',
'switch',
'throws',
'throw',
'try\\?', // operator
'try!', // operator
'try', // operator
'typealias',
'unowned\\(safe\\)', // contextual
'unowned\\(unsafe\\)', // contextual
'unowned', // contextual
'var',
'weak', // contextual
'where',
'while',
'willSet' // contextual
].map(keywordWrapper);

// NOTE: Contextual keywords are reserved only in specific contexts.
// Ideally, these should be matched using modes to avoid false positives.

// TODO: Create a PRECEDENCE_GROUP mode to match the remaining contextual keywords:
// assignment associativity higherThan left lowerThan none right
// These aren't included in the list because they result in mostly false positives.

// Literals.
export const literals = [
'false',
'nil',
'true'
].map(keywordWrapper);

// Keywords that start with a number sign (#).
// #available is handled separately.
export const numberSignKeywords = [
'colorLiteral',
'column',
'dsohandle',
'else',
'elseif',
'endif',
'error',
'file',
'fileID',
'fileLiteral',
'filePath',
'function',
'if',
'imageLiteral',
'keyPath',
'line',
'selector',
'sourceLocation',
'warn_unqualified_access',
'warning'
];

// Global functions in the Standard Library.
export const builtIns = [
'abs',
'all',
'any',
'assert',
'assertionFailure',
'debugPrint',
'dump',
'fatalError',
'getVaList',
'isKnownUniquelyReferenced',
'max',
'min',
'numericCast',
'pointwiseMax',
'pointwiseMin',
'precondition',
'preconditionFailure',
'print',
'readLine',
'repeatElement',
'sequence',
'stride',
'swap',
'swift_unboxFromSwiftValueWithType',
'transcode',
'type',
'unsafeBitCast',
'unsafeDowncast',
'withExtendedLifetime',
'withUnsafeMutablePointer',
'withUnsafePointer',
'withVaList',
'withoutActuallyEscaping',
'zip'
];

// Valid first characters for operators.
export const operatorHead = either(
/[/=\-+!*%<>&|^~?]/,
/[\u00A1-\u00A7]/,
/[\u00A9\u00AB]/,
/[\u00AC\u00AE]/,
/[\u00B0\u00B1]/,
/[\u00B6\u00BB\u00BF\u00D7\u00F7]/,
/[\u2016-\u2017]/,
/[\u2020-\u2027]/,
/[\u2030-\u203E]/,
/[\u2041-\u2053]/,
/[\u2055-\u205E]/,
/[\u2190-\u23FF]/,
/[\u2500-\u2775]/,
/[\u2794-\u2BFF]/,
/[\u2E00-\u2E7F]/,
/[\u3001-\u3003]/,
/[\u3008-\u3020]/,
/[\u3030]/
);

// Valid characters for operators.
export const operatorCharacter = either(
operatorHead,
/[\u0300-\u036F]/,
/[\u1DC0-\u1DFF]/,
/[\u20D0-\u20FF]/,
/[\uFE00-\uFE0F]/,
/[\uFE20-\uFE2F]/
// TODO: The following characters are also allowed, but the regex isn't supported yet.
// /[\u{E0100}-\u{E01EF}]/u
);

// Valid operator.
export const operator = concat(operatorHead, operatorCharacter, '*');

// Valid first characters for identifiers.
export const identifierHead = either(
/[a-zA-Z_]/,
/[\u00A8\u00AA\u00AD\u00AF\u00B2-\u00B5\u00B7-\u00BA]/,
/[\u00BC-\u00BE\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF]/,
/[\u0100-\u02FF\u0370-\u167F\u1681-\u180D\u180F-\u1DBF]/,
/[\u1E00-\u1FFF]/,
/[\u200B-\u200D\u202A-\u202E\u203F-\u2040\u2054\u2060-\u206F]/,
/[\u2070-\u20CF\u2100-\u218F\u2460-\u24FF\u2776-\u2793]/,
/[\u2C00-\u2DFF\u2E80-\u2FFF]/,
/[\u3004-\u3007\u3021-\u302F\u3031-\u303F\u3040-\uD7FF]/,
/[\uF900-\uFD3D\uFD40-\uFDCF\uFDF0-\uFE1F\uFE30-\uFE44]/,
/[\uFE47-\uFFFD]/
// The following characters are also allowed, but the regexes aren't supported yet.
// /[\u{10000}-\u{1FFFD}\u{20000-\u{2FFFD}\u{30000}-\u{3FFFD}\u{40000}-\u{4FFFD}]/u,
// /[\u{50000}-\u{5FFFD}\u{60000-\u{6FFFD}\u{70000}-\u{7FFFD}\u{80000}-\u{8FFFD}]/u,
// /[\u{90000}-\u{9FFFD}\u{A0000-\u{AFFFD}\u{B0000}-\u{BFFFD}\u{C0000}-\u{CFFFD}]/u,
// /[\u{D0000}-\u{DFFFD}\u{E0000-\u{EFFFD}]/u
);

// Valid characters for identifiers.
export const identifierCharacter = either(
identifierHead,
/\d/,
/[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/
Copy link
Member

@joshgoebel joshgoebel Dec 12, 2020

Choose a reason for hiding this comment

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

I'm showing \uFE20-\uFE2F (the whole range) as problematic because it's a combining character. Unless you want to add a specific test that proves this works as it should we should remove it/comment it out. (I'm trusting that the linter is right on this and this is broken)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes this works and is valid Swift code:

infix operator ~︦ // ~ 0xFE26

func ~︦ (lhs: Int, rhs: Int) -> Int {
    lhs + rhs
}

1 ~︦ 2

Screen Shot 2020-12-13 at 13 07 35

Should I add this as a unit test?

I assume this isn't a problem here because there are different rules for the first character of an operator. I'm not familiar with these ranges, but I assume the rules for operatorHead don't include combining characters?

Copy link
Member

Choose a reason for hiding this comment

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

It wouldn't hurt. Do you have any idea what that warning is trying to tell us? Can the character also be used to make a compound character and then that doesn't work?

Copy link
Member

Choose a reason for hiding this comment

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

but I assume the rules for operatorHead don't include combining characters?

This one regex is the only one still being flagged and just that portion of it. So it's unique somehow.

);

// Valid identifier.
export const identifier = concat(identifierHead, identifierCharacter, '*');

// Built-in attributes, which are highlighted as keywords.
// @available is handled separately.
export const keywordAttributes = [
'autoclosure',
concat(/convention\(/, either('swift', 'block', 'c'), /\)/),
'discardableResult',
'dynamicCallable',
'dynamicMemberLookup',
'escaping',
'frozen',
'GKInspectable',
'IBAction',
'IBDesignable',
'IBInspectable',
'IBOutlet',
'IBSegueAction',
'inlinable',
'main',
'nonobjc',
'NSApplicationMain',
'NSCopying',
'NSManaged',
concat(/objc\(/, identifier, /\)/),
'objc',
'objcMembers',
'propertyWrapper',
'requires_stored_property_inits',
'testable',
'UIApplicationMain',
'unknown',
'usableFromInline'
];

// Contextual keywords used in @available and #available.
export const availabilityKeywords = [
'iOS',
'iOSApplicationExtension',
'macOS',
'macOSApplicationExtension',
'macCatalyst',
'macCatalystApplicationExtension',
'watchOS',
'watchOSApplicationExtension',
'tvOS',
'tvOSApplicationExtension',
'swift'
];
Loading