diff --git a/__tests__/typer.ts b/__tests__/typer.ts index 6321566..f704d7e 100644 --- a/__tests__/typer.ts +++ b/__tests__/typer.ts @@ -3,10 +3,10 @@ import typing, { Type } from '../src/typer'; describe('typing', () => { it('types combinators', () => { - expect(typing(parse('something another-thing'))).toHaveLength(1); - expect(typing(parse('something && another-thing'))).toHaveLength(1); - expect(typing(parse('something || another-thing'))).toHaveLength(3); expect(typing(parse('something | another-thing'))).toHaveLength(2); + expect(typing(parse('something || another-thing'))).toHaveLength(4); + expect(typing(parse('something && another-thing'))).toHaveLength(2); + expect(typing(parse('something another-thing'))).toHaveLength(1); }); it('types components', () => { @@ -26,52 +26,53 @@ describe('typing', () => { }); it('types optional components', () => { - expect(typing(parse('something another-thing? | 100'))).toMatchObject([ - { type: Type.StringLiteral }, - { type: Type.String }, - { type: Type.NumericLiteral }, - ]); - expect(typing(parse('something another-thing? yet-another-thing? | 100'))).toMatchObject([ - { type: Type.StringLiteral }, - { type: Type.String }, - { type: Type.NumericLiteral }, + expect(typing(parse('something another-thing?'))).toMatchObject([ + { type: Type.StringLiteral, literal: 'something' }, + { type: Type.StringLiteral, literal: 'something another-thing' }, ]); - expect(typing(parse('something? another-thing yet-another-thing? | 100'))).toMatchObject([ - { type: Type.String }, - { type: Type.StringLiteral }, - { type: Type.NumericLiteral }, + expect(typing(parse('something another-thing? yet-another-thing?'))).toMatchObject([ + { type: Type.StringLiteral, literal: 'something another-thing yet-another-thing' }, + { type: Type.StringLiteral, literal: 'something another-thing' }, + { type: Type.StringLiteral, literal: 'something yet-another-thing' }, + { type: Type.StringLiteral, literal: 'something' }, ]); - expect(typing(parse('something? another-thing? yet-another-thing | 100'))).toMatchObject([ - { type: Type.String }, - { type: Type.StringLiteral }, - { type: Type.NumericLiteral }, + expect(typing(parse('something? another-thing yet-another-thing?'))).toMatchObject([ + { type: Type.StringLiteral, literal: 'something another-thing yet-another-thing' }, + { type: Type.StringLiteral, literal: 'something another-thing' }, + { type: Type.StringLiteral, literal: 'another-thing yet-another-thing' }, + { type: Type.StringLiteral, literal: 'another-thing' }, ]); - expect(typing(parse('something? another-thing? yet-another-thing? | 100'))).toMatchObject([ - { type: Type.StringLiteral }, - { type: Type.String }, - { type: Type.StringLiteral }, - { type: Type.StringLiteral }, - { type: Type.NumericLiteral }, + expect(typing(parse('something? another-thing? yet-another-thing'))).toMatchObject([ + { type: Type.StringLiteral, literal: 'something another-thing yet-another-thing' }, + { type: Type.StringLiteral, literal: 'something yet-another-thing' }, + { type: Type.StringLiteral, literal: 'another-thing yet-another-thing' }, + { type: Type.StringLiteral, literal: 'yet-another-thing' }, ]); - expect(typing(parse('something another-thing yet-another-thing? | 100'))).toMatchObject([ - { type: Type.String }, - { type: Type.NumericLiteral }, + expect(typing(parse('something? another-thing? yet-another-thing?'))).toMatchObject([ + { type: Type.StringLiteral, literal: 'something another-thing yet-another-thing' }, + { type: Type.StringLiteral, literal: 'something another-thing' }, + { type: Type.StringLiteral, literal: 'something yet-another-thing' }, + { type: Type.StringLiteral, literal: 'another-thing yet-another-thing' }, + { type: Type.StringLiteral, literal: 'something yet-another-thing' }, + { type: Type.StringLiteral, literal: 'something' }, + { type: Type.StringLiteral, literal: 'another-thing' }, + { type: Type.StringLiteral, literal: 'yet-another-thing' }, ]); - expect(typing(parse('something another-thing? yet-another-thing | 100'))).toMatchObject([ - { type: Type.String }, - { type: Type.NumericLiteral }, + expect(typing(parse('something another-thing yet-another-thing?'))).toMatchObject([ + { type: Type.StringLiteral, literal: 'something another-thing yet-another-thing' }, + { type: Type.StringLiteral, literal: 'something another-thing' }, ]); - expect(typing(parse('something? another-thing yet-another-thing | 100'))).toMatchObject([ - { type: Type.String }, - { type: Type.NumericLiteral }, + expect(typing(parse('something another-thing? yet-another-thing'))).toMatchObject([ + { type: Type.StringLiteral, literal: 'something another-thing yet-another-thing' }, + { type: Type.StringLiteral, literal: 'something yet-another-thing' }, ]); }); it('types optional group components', () => { - expect(typing(parse('[ something ] [ another-thing ]? | 100'))).toMatchObject([ - { type: Type.StringLiteral }, - { type: Type.String }, - { type: Type.NumericLiteral }, + expect(typing(parse('something [ another-thing | yet-another-thing ]?'))).toMatchObject([ + { type: Type.StringLiteral, literal: 'something another-thing' }, + { type: Type.StringLiteral, literal: 'something yet-another-thing' }, + { type: Type.StringLiteral, literal: 'something' }, ]); }); }); diff --git a/src/at-rules.ts b/src/at-rules.ts index 2543d15..636bcd8 100644 --- a/src/at-rules.ts +++ b/src/at-rules.ts @@ -69,7 +69,7 @@ export let getAtRules = () => { } } - // Cache + // Memoize getAtRules = () => ({ literals, rules, diff --git a/src/comment.ts b/src/comment.ts index 48b05e7..8901dc7 100644 --- a/src/comment.ts +++ b/src/comment.ts @@ -123,7 +123,7 @@ function getCompatRows(compatibilityData: MDN.CompatData) { } function supportVersion(supports: MDN.Support | MDN.Support[] | undefined): string[] { - supports = supports ? (Array.isArray(supports) ? supports : [supports]).reverse() : []; + supports = supports ? (Array.isArray(supports) ? supports : [supports]) : []; // Ignore versions hidden under flags supports = supports.filter(({ flags }) => !flags); @@ -251,7 +251,7 @@ function formatL10n(phrase: string) { } if (chunk.startsWith('{{') && chunk.endsWith('}}')) { - warn('Unknown curly bracket block `%s` in i10n', chunk); + warn('Unknown curly braces block `%s` in i10n', chunk); return chunk; } diff --git a/src/data.ts b/src/data.ts index e1ee0a3..d1b3525 100644 --- a/src/data.ts +++ b/src/data.ts @@ -28,7 +28,7 @@ export let getProperties = () => { } } - // Cache + // Memoize getProperties = () => properties; return properties; @@ -55,7 +55,7 @@ export let getSyntaxes = () => { } } - // Cache + // Memoize getSyntaxes = () => syntaxes; return syntaxes; diff --git a/src/parser.ts b/src/parser.ts index 90ff6e9..ac5f31c 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -183,18 +183,18 @@ export function isCombinator(entity: EntityType): entity is ICombinator { return entity.entity === Entity.Combinator; } -export function isCurlyBracetMultiplier(multiplier: MultiplierType): multiplier is IMultiplierCurlyBracet { +export function isCurlyBracesMultiplier(multiplier: MultiplierType): multiplier is IMultiplierCurlyBracet { return multiplier.sign === Multiplier.CurlyBracet; } export function isMandatoryMultiplied(multiplier: MultiplierType | null) { - return multiplier !== null && (isCurlyBracetMultiplier(multiplier) && multiplier.min > 1); + return multiplier !== null && (isCurlyBracesMultiplier(multiplier) && multiplier.min > 1); } export function isOptionallyMultiplied(multiplier: MultiplierType | null) { return ( multiplier !== null && - ((isCurlyBracetMultiplier(multiplier) && multiplier.min < multiplier.max && multiplier.max > 1) || + ((isCurlyBracesMultiplier(multiplier) && multiplier.min < multiplier.max && multiplier.max > 1) || multiplier.sign === Multiplier.Asterisk || multiplier.sign === Multiplier.PlusSign || multiplier.sign === Multiplier.HashMark || @@ -209,7 +209,7 @@ export function isMandatoryEntity(entity: EntityType) { if (entity.multiplier) { return ( - (isCurlyBracetMultiplier(entity.multiplier) && entity.multiplier.min > 0) || + (isCurlyBracesMultiplier(entity.multiplier) && entity.multiplier.min > 0) || entity.multiplier.sign === Multiplier.PlusSign || entity.multiplier.sign === Multiplier.HashMark || entity.multiplier.sign === Multiplier.ExclamationPoint @@ -263,6 +263,24 @@ function multiplierData(raw: string[]): MultiplierType | null { } } +export function precedenceCombinator(entities: EntityType[]) { + let combinator: ICombinator | null = null; + + for (const entity of entities) { + if (isCombinator(entity)) { + if (!combinator) { + combinator = entity; + } + if (combinator !== entity) { + // This should never happen if grouping works as it should. So we just wnt to make sure. + throw new Error('Combinators must be grouped by precedence'); + } + } + } + + return combinator; +} + function groupByPrecedence(entities: EntityType[], precedence: number = Combinator.SingleBar): EntityType[] { if (precedence < 0) { // We've reached the lowest precedence possible diff --git a/src/selectors.ts b/src/selectors.ts index 9eef1a9..4ed3bd1 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -26,7 +26,7 @@ export let getPseudos = () => { } } } - // Cache + // Memoize getPseudos = () => ({ simple, advanced, diff --git a/src/typer.ts b/src/typer.ts index 07960d5..e54122c 100644 --- a/src/typer.ts +++ b/src/typer.ts @@ -1,7 +1,7 @@ import * as cssTypes from 'mdn-data/css/types.json'; -import { isProperty, isSyntax } from './data'; +import { isProperty, isSyntax, getSyntax, getPropertySyntax } from './data'; import { warn } from './logger'; -import { +import parse, { Combinator, Component, EntityType, @@ -10,6 +10,8 @@ import { isMandatoryEntity, isMandatoryMultiplied, isOptionallyMultiplied, + isCurlyBracesMultiplier, + precedenceCombinator, } from './parser'; export enum Type { @@ -49,6 +51,8 @@ export type TypeType = IBasic | IStringLiteral | INumeric export type ResolvedType = TypeType; +const CURLY_BRACES_MULTIPLIER_MAXIMUM = 3; + let getBasicDataTypes = () => { const types = Object.keys(cssTypes).reduce<{ [name: string]: IBasic }>((dataTypes, name) => { switch (name) { @@ -73,13 +77,19 @@ let getBasicDataTypes = () => { return dataTypes; }, {}); - // Cache + // Memoize getBasicDataTypes = () => types; return types; }; export default function typing(entities: EntityType[]): TypeType[] { + const strictTypes = strictTyping(entities); + + if (strictTypes !== null) { + return strictTypes; + } + let mandatoryCombinatorCount = 0; let mandatoryNonCombinatorsCount = 0; for (const entity of entities) { @@ -170,6 +180,55 @@ export default function typing(entities: EntityType[]): TypeType[] { return types; } +export function strictTyping(entities: EntityType[]): TypeType[] | null { + const types: TypeType[] = []; + const combinator = precedenceCombinator(entities); + + for (const entity of entities) { + if (isComponent(entity)) { + switch (entity.component) { + case Component.DataType: { + if (isSyntax(entity.value) || isProperty(entity.value)) { + const strictTypes = strictTyping( + parse(isSyntax(entity.value) ? getSyntax(entity.value) : getPropertySyntax(entity.value)), + ); + + if (strictTypes === null) { + return null; + } + } + + // Missing or basic data type + return null; + } + case Component.Group: { + const strictTypes = strictTyping(entity.entities); + + if (strictTypes === null) { + return null; + } + + // TODO + + break; + } + } + + if ( + entity.multiplier !== null && + // We can work with a small amount. But too many isn't worth it. + !(isCurlyBracesMultiplier(entity.multiplier) && entity.multiplier.max < CURLY_BRACES_MULTIPLIER_MAXIMUM) + ) { + return null; + } + + continue; + } + } + + return types; +} + function addLength(types: Array>): Array> { if (types.every(type => type.type !== Type.Length)) { return [