Skip to content
Open
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
79 changes: 40 additions & 39 deletions __tests__/typer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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' },
]);
});
});
2 changes: 1 addition & 1 deletion src/at-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export let getAtRules = () => {
}
}

// Cache
// Memoize
getAtRules = () => ({
literals,
rules,
Expand Down
4 changes: 2 additions & 2 deletions src/comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}

Expand Down
4 changes: 2 additions & 2 deletions src/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export let getProperties = () => {
}
}

// Cache
// Memoize
getProperties = () => properties;

return properties;
Expand All @@ -55,7 +55,7 @@ export let getSyntaxes = () => {
}
}

// Cache
// Memoize
getSyntaxes = () => syntaxes;

return syntaxes;
Expand Down
26 changes: 22 additions & 4 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export let getPseudos = () => {
}
}
}
// Cache
// Memoize
getPseudos = () => ({
simple,
advanced,
Expand Down
65 changes: 62 additions & 3 deletions src/typer.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -10,6 +10,8 @@ import {
isMandatoryEntity,
isMandatoryMultiplied,
isOptionallyMultiplied,
isCurlyBracesMultiplier,
precedenceCombinator,
} from './parser';

export enum Type {
Expand Down Expand Up @@ -49,6 +51,8 @@ export type TypeType<TDataType = IDataType> = IBasic | IStringLiteral | INumeric

export type ResolvedType = TypeType<DataType>;

const CURLY_BRACES_MULTIPLIER_MAXIMUM = 3;

let getBasicDataTypes = () => {
const types = Object.keys(cssTypes).reduce<{ [name: string]: IBasic }>((dataTypes, name) => {
switch (name) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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<TDataType extends IDataType>(types: Array<TypeType<TDataType>>): Array<TypeType<TDataType>> {
if (types.every(type => type.type !== Type.Length)) {
return [
Expand Down