This commit is contained in:
markcowl 2021-06-21 18:59:59 -07:00
Родитель 341baba352 fec91682a8
Коммит 2a4b85b888
101 изменённых файлов: 6442 добавлений и 1667 удалений

Просмотреть файл

@ -96,6 +96,34 @@ model Pet {
model Dog extends Pet, Animal { }
```
### Enums
Enums define a type which can hold one of a set of constant values.
```
enum Color {
Red,
Blue,
Green,
}
```
In this case, we haven't specified how the constants will be represented, allowing for different choices in different scenarios. For example, the OpenAPI emitter will choose string values "Red", "Green", "Blue". Another protocol might prefer to assign incrementing numeric values 0, 1, 2.
We can also specify explicit string or numeric values:
```
enum Color {
Red: "red",
Blue: "blue"
Green: "green",
}
enum Priority {
High: 100,
Low: 0,
}
```
#### Templates
It is often useful to let the users of a model fill in certain details. Model templates enable this pattern. Similar to generics found in other languages, model templates declare template parameters that users provide when referencing the model.
@ -111,14 +139,16 @@ model DogPage {
}
```
#### Model Aliases
#### Type Aliases
Sometimes it's convenient to alias a model template instantiation or model produced via type operators (covered later) as a convenient name. Model aliases allow this:
Sometimes it's convenient to alias a model template instantiation or type produced via type operators (covered later) as a convenient name. Aliases allow this:
```
model DogPage = Page<Dog>;
alias DogPage = Page<Dog>;
```
Unlike `model`, `alias` does not create a new entity, and as such will not change generated code in any way. An alias merely describes a source code shorthand to avoid repeating the right-hand side in multiple places.
### Type Literals
API authors often need to describe API shapes in terms of specific literal values. For example, this operation returns this specific integer status code, or this model member can be one of a few specific string values. It is also often useful to pass specific literal values to decorators. ADL supports string, number, and boolean literal values to support these cases:
@ -153,7 +183,7 @@ ADL supports a few type operators that make it easy to compose new models from o
Unions describe a type that must be exactly one of the union's constituents. Create a union with the `|` operator.
```
model GoodBreeds = 'Beagle' | 'German Shepherd' | 'Golden Retriever';
alias GoodBreed = Beagle | GermanShepherd | GoldenRetriever;
```
#### Intersection
@ -161,7 +191,7 @@ model GoodBreeds = 'Beagle' | 'German Shepherd' | 'Golden Retriever';
Intersections describe a type that must include all of the intersection's constituents. Create an intersection with the `&` operator.
```
model Dog = Animal & Pet;
alias Dog = Animal & Pet;
```
#### Array
@ -169,7 +199,7 @@ model Dog = Animal & Pet;
Arrays describe lists of things. Create an Array type with the `[]` operator.
```
model Pack = Dog[];
alias Pack = Dog[];
```
### Operations
@ -228,7 +258,7 @@ namespace A.B;
namespace C.D {}
namespace C.D.E { model M { }}
model M = A.B.C.D.E.M;
alias M = A.B.C.D.E.M;
```
It can be convenient to add references to a namespace's declarations to your local namespace, especially when namespaces can become deeply nested. The `using` statement lets us do this:
@ -253,11 +283,11 @@ namespace Test {
namespace Test2 {
using Test;
model B = A; // ok
alias B = A; // ok
}
model C = Test2.A; // not ok
model C = Test2.B; // ok
alias C = Test2.A; // not ok
alias C = Test2.B; // ok
```
### Imports

Просмотреть файл

@ -116,7 +116,7 @@ function getUrl(pkg) {
}
async function getLicense(packageRoot) {
for (const licenseName of ["LICENSE", "LICENSE.txt", "LICENSE.md"]) {
for (const licenseName of ["LICENSE", "LICENSE.txt", "LICENSE.md", "LICENSE-MIT"]) {
const licensePath = join(packageRoot, licenseName);
try {
let text = (await readFile(licensePath)).toString("utf-8");

Просмотреть файл

@ -1,6 +1,6 @@
import { spawn, spawnSync } from "child_process";
import { readFileSync } from "fs";
import { dirname, resolve } from "path";
import { statSync, readFileSync } from "fs";
import { dirname, join, resolve } from "path";
import { fileURLToPath } from "url";
function read(filename) {
@ -31,7 +31,11 @@ export function forEachProject(onEach) {
export function npmForEach(cmd, options) {
forEachProject((name, location, project) => {
// checks for the script first
if (cmd === "test-official" && !project.scripts[cmd] && project.scripts["test"]) {
const pj = join(location, "package.json");
throw new Error(`${pj} has a 'test' script, but no 'test-official' script for CI.`);
}
if (project.scripts[cmd] || cmd === "pack") {
const args = cmd === "pack" ? [cmd] : ["run", cmd];
run("npm", args, { cwd: location, ...options });
@ -105,41 +109,75 @@ export function clearScreen() {
}
export function runWatch(watch, dir, build, options) {
let lastStartTime;
let lastBuildTime;
dir = resolve(dir);
// build once up-front.
runBuild();
// We need to wait for directory to be created before watching it. This deals
// with races between watchers where one watcher must create a directory
// before another can watch it.
//
// For example, we can't watch for tmlanguage.js changes if the source watcher
// hasn't even created the directory in which tmlanguage.js will be written.
try {
statSync(dir);
} catch (err) {
if (err.code === "ENOENT") {
waitForDirectoryCreation();
return;
}
throw err;
}
watch.createMonitor(dir, { interval: 0.2, ...options }, (monitor) => {
let handler = function (file) {
if (lastStartTime && monitor?.files[file]?.mtime < lastStartTime) {
// File was changed before last build started so we can ignore it. This
// avoids running the build unnecessarily when a series of input files
// change at the same time.
return;
}
runBuild(file);
};
// Directory already exists: we can start watching right away.
start();
monitor.on("created", handler);
monitor.on("changed", handler);
monitor.on("removed", handler);
});
function waitForDirectoryCreation() {
let dirCreated = false;
let parentDir = dirname(dir);
logWithTime(`Waiting for ${dir} to be created.`);
function runBuild(file) {
runBuildAsync(file).catch((err) => {
watch.createMonitor(parentDir, "created", (monitor) => {
monitor.on("created", (file) => {
if (!dirCreated && file === dir) {
dirCreated = true; // defend against duplicate events.
monitor.stop();
start();
}
});
});
}
function start() {
// build once up-front
runBuild();
// then build again on any change
watch.createMonitor(dir, { interval: 0.2, ...options }, (monitor) => {
monitor.on("created", (file) => runBuild(`${file} created`));
monitor.on("removed", (file) => runBuild(`${file} removed`));
monitor.on("changed", (file) => runBuild(`${file} changed`, monitor.files[file]?.mtime));
});
}
function runBuild(changeDescription, changeTime) {
runBuildAsync(changeDescription, changeTime).catch((err) => {
console.error(err.stack);
process.exit(1);
});
}
async function runBuildAsync(file) {
lastStartTime = Date.now();
clearScreen();
async function runBuildAsync(changeDescription, changeTime) {
if (changeTime && lastBuildTime && changeTime < lastBuildTime) {
// Don't rebuild if a change happened before the last build kicked off.
// Defends against duplicate events and building more than once when a
// bunch of files are changed at the same time.
return;
}
if (file) {
logWithTime(`File change detected: ${file}. Running build.`);
lastBuildTime = new Date();
if (changeDescription) {
clearScreen();
logWithTime(`File change detected: ${changeDescription}. Running build.`);
} else {
logWithTime("Starting build in watch mode.");
}

Просмотреть файл

@ -16,7 +16,7 @@
},
"type": "module",
"scripts": {
"build": "ecmarkup src/spec.emu.html ../../docs/spec.html",
"build": "ecmarkup --strict src/spec.emu.html ../../docs/spec.html",
"watch": "node scripts/watch-spec.js"
},
"dependencies": {},

Просмотреть файл

@ -1,6 +1,6 @@
import watch from "watch";
import ecmarkup from "ecmarkup";
import { readFile } from "fs/promises";
import { readFile, writeFile } from "fs/promises";
import { runWatch } from "../../../eng/scripts/helpers.js";
import { resolve } from "path";
@ -10,11 +10,25 @@ async function build() {
const fetch = (path) => readFile(path, "utf-8");
try {
await ecmarkup.build(infile, fetch, { outfile });
const spec = await ecmarkup.build(infile, fetch, {
outfile,
warn,
});
for (const [file, contents] of spec.generatedFiles) {
await writeFile(file, contents);
}
} catch (err) {
console.log(`${infile}(1,1): error EMU0001: Error generating spec: ${err.message}`);
throw err;
}
function warn(warning) {
const file = warning.file ?? infile;
const line = warning.line ?? 1;
const col = warning.column ?? 1;
const id = "EMU0002" + (warning.ruleId ? `: ${warning.ruleId}` : "");
console.log(`${file}(${line},${col}): warning ${id}: ${warning.message}`);
}
}
runWatch(watch, "src", build);

Просмотреть файл

@ -65,13 +65,16 @@ BooleanLiteral :
NumericLiteral :
DecimalLiteral
HexIntegerLiteral
BinaryIntegerLiteral
DecimalLiteral :
DecimalIntegerLiteral `.` DecimalDigits? ExponentPart?
DecimalIntegerLiteral `.` DecimalDigits ExponentPart?
DecimalIntegerLiteral ExponentPart?
DecimalIntegerLiteral :
DecimalDigits
`+` DecimalDigits
`-` DecimalDigits
DecimalDigits :
DecimalDigit
@ -81,9 +84,9 @@ DecimalDigit :
one of `0` `1` `2` `3` `4` `5` `6` `7` `8` `9`
ExponentPart :
`e` SignedInteger
`e` DecimalIntegerLiteral
SignedInteger :
DecimalIntegerInteger :
DecimalDigits
`+` DecimalDigits
`-` DecimalDigits
@ -108,15 +111,23 @@ BinaryDigits :
BinaryDigit :
one of `0` `1`
// TODO: triple-quoted strings not specified yet, tricky to express.
// NOTE: This does not specify the extra rules about '"""'s going
// on their own lines and having consistent indentation.
StringLiteral :
`"` StringCharacters? `"`
`"""` TripleQuotedStringCharacters? `"""`
StringCharacters :
StringCharacter StringCharacters?
StringCharacter :
SourceCharacter but not one of `"` or `\` or LineTerminator
`\` EscapeCharacter
TripleQuotedStringCharacters
TripleQuotedStringCharacter TripleQuotedStringCharacters?
TripleQuotedStringCharacter :
SourceCharacter but not one of `"` or `\`
`\` EscapeCharacter
@ -203,6 +214,8 @@ Statement :
NamespaceStatement
OperationStatement
UsingStatement
EnumStatement
AliasStatement
`;`
UsingStatement :
@ -210,22 +223,10 @@ UsingStatement :
ModelStatement :
DecoratorList? `model` Identifier TemplateParameters? ModelHeritage? `{` ModelBody? `}`
DecoratorList? `model` Identifier TemplateParameters? `=` Expression `;`
ModelHeritage :
`extends` ReferenceExpressionList
ReferenceExpressionList :
ReferenceExpression
ReferenceExpressionList `,` ReferenceExpression
TemplateParameters :
`<` IdentifierList `>`
IdentifierList :
Identifier
IdentifierList `,` Identifier
ModelBody :
ModelPropertyList `,`?
ModelPropertyList `;`?
@ -243,6 +244,40 @@ ModelProperty:
ModelSpreadProperty :
`...` ReferenceExpression
EnumStatement :
DecoratorList? `enum` Identifier `{` EnumBody? `}`
EnumBody :
EnumMemberList `,`?
EnumMemberList `;`?
EnumMemberList :
EnumMember
EnumMemberList `,` EnumMember
EnumMemberList `;` EnumMember
EnumMember :
DecoratorList? Identifier EnumMemberValue?
DecoratorList? StringLiteral EnumMemberValue?
EnumMemberValue :
`:` StringLiteral
`:` NumericLiteral
AliasStatement :
`alias` Identifier TemplateParameters? `=` Expression;
ReferenceExpressionList :
ReferenceExpression
ReferenceExpressionList `,` ReferenceExpression
TemplateParameters :
`<` IdentifierList `>`
IdentifierList :
Identifier
IdentifierList `,` Identifier
NamespaceStatement:
DecoratorList? `namespace` IdentifierOrMemberExpression `{` StatementList? `}`

Просмотреть файл

@ -1,6 +1,43 @@
{
"name": "@azure-tools/adl-rest",
"entries": [
{
"version": "0.2.1",
"tag": "@azure-tools/adl-rest_v0.2.1",
"date": "Tue, 18 May 2021 23:43:31 GMT",
"comments": {
"dependency": [
{
"comment": "Updating dependency \"@azure-tools/adl\" from `0.10.0` to `0.11.0`"
}
]
}
},
{
"version": "0.2.0",
"tag": "@azure-tools/adl-rest_v0.2.0",
"date": "Thu, 06 May 2021 14:56:01 GMT",
"comments": {
"minor": [
{
"comment": "Implement alias and enum, remove model ="
}
],
"patch": [
{
"comment": "**Added** New type NoContentResponse"
},
{
"comment": "Replace several internal compiler errors with diagnostics"
}
],
"dependency": [
{
"comment": "Updating dependency \"@azure-tools/adl\" from `0.9.0` to `0.10.0`"
}
]
}
},
{
"version": "0.1.2",
"tag": "@azure-tools/adl-rest_v0.1.2",

Просмотреть файл

@ -1,6 +1,23 @@
# Change Log - @azure-tools/adl-rest
This log was last generated on Tue, 20 Apr 2021 15:23:29 GMT and should not be manually modified.
This log was last generated on Tue, 18 May 2021 23:43:31 GMT and should not be manually modified.
## 0.2.1
Tue, 18 May 2021 23:43:31 GMT
_Version update only_
## 0.2.0
Thu, 06 May 2021 14:56:01 GMT
### Minor changes
- Implement alias and enum, remove model =
### Patches
- **Added** New type NoContentResponse
- Replace several internal compiler errors with diagnostics
## 0.1.2
Tue, 20 Apr 2021 15:23:29 GMT

Просмотреть файл

@ -1,6 +1,6 @@
{
"name": "@azure-tools/adl-rest",
"version": "0.1.2",
"version": "0.2.1",
"author": "Microsoft Corporation",
"description": "ADL REST protocol binding",
"homepage": "https://github.com/Azure/adl",
@ -32,10 +32,10 @@
"!dist/test/**"
],
"dependencies": {
"@azure-tools/adl": "0.9.0"
"@azure-tools/adl": "0.11.0"
},
"devDependencies": {
"@types/node": "~14.0.27",
"typescript": "~4.2.4"
"typescript": "~4.3.2"
}
}

Просмотреть файл

@ -1,7 +1,6 @@
import { NamespaceType, OperationType, Program, throwDiagnostic, Type } from "@azure-tools/adl";
const basePaths = new Map<Type, string>();
const basePathsKey = Symbol();
export interface HttpOperationType extends OperationType {
basePath: string,
route: OperationRoute
@ -25,45 +24,46 @@ export function getHttpOperation(operation: OperationType): HttpOperationType |
export function resource(program: Program, entity: Type, basePath = "") {
if (entity.kind !== "Namespace") return;
basePaths.set(entity, basePath);
program.stateMap(basePathsKey).set(entity, basePath);
}
export function getResources() {
return Array.from(basePaths.keys());
export function getResources(program: Program) {
return Array.from(program.stateMap(basePathsKey).keys());
}
export function isResource(obj: Type) {
return basePaths.has(obj);
export function isResource(program: Program, obj: Type) {
return program.stateMap(basePathsKey).has(obj);
}
export function basePathForResource(resource: Type) {
return basePaths.get(resource);
export function basePathForResource(program: Program, resource: Type) {
return program.stateMap(basePathsKey).get(resource);
}
const headerFields = new Map<Type, string>();
const headerFieldsKey = Symbol();
export function header(program: Program, entity: Type, headerName: string) {
if (!headerName && entity.kind === "ModelProperty") {
headerName = entity.name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
}
headerFields.set(entity, headerName);
program.stateMap(headerFieldsKey).set(entity, headerName);
}
export function getHeaderFieldName(entity: Type) {
return headerFields.get(entity);
export function getHeaderFieldName(program: Program, entity: Type) {
return program.stateMap(headerFieldsKey).get(entity);
}
const queryFields = new Map<Type, string>();
const queryFieldsKey = Symbol();
export function query(program: Program, entity: Type, queryKey: string) {
if (!queryKey && entity.kind === "ModelProperty") {
queryKey = entity.name;
}
queryFields.set(entity, queryKey);
program.stateMap(queryFieldsKey).set(entity, queryKey);
}
export function getQueryParamName(entity: Type) {
return queryFields.get(entity);
export function getQueryParamName(program: Program, entity: Type) {
return program.stateMap(queryFieldsKey).get(entity);
}
const pathFieldsKey = Symbol();
export function isQueryParam(entity: Type) {
return queryFields.has(entity);
}
@ -73,24 +73,25 @@ export function path(program: Program, entity: Type, paramName: string) {
if (!paramName && entity.kind === "ModelProperty") {
paramName = entity.name;
}
pathFields.set(entity, paramName);
program.stateMap(pathFieldsKey).set(entity, paramName);
}
export function getPathParamName(entity: Type) {
return pathFields.get(entity);
export function getPathParamName(program: Program, entity: Type) {
return program.stateMap(pathFieldsKey).get(entity);
}
const bodyFieldsKey = Symbol();
export function isPathParam(entity: Type) {
return pathFields.has(entity);
}
const bodyFields = new Set<Type>();
export function body(program: Program, entity: Type) {
bodyFields.add(entity);
program.stateSet(bodyFieldsKey).add(entity);
}
export function isBody(entity: Type) {
return bodyFields.has(entity);
export function isBody(program: Program, entity: Type) {
return program.stateSet(bodyFieldsKey).has(entity);
}
export type HttpVerb = "get" | "put" | "post" | "patch" | "delete";
@ -100,47 +101,47 @@ interface OperationRoute {
subPath?: string;
}
const operationRoutes = new Map<Type, OperationRoute>();
const operationRoutesKey = Symbol();
function setOperationRoute(entity: Type, verb: OperationRoute) {
function setOperationRoute(program: Program, entity: Type, verb: OperationRoute) {
if (entity.kind === "Operation") {
if (!operationRoutes.has(entity)) {
operationRoutes.set(entity, verb);
if (!program.stateMap(operationRoutesKey).has(entity)) {
program.stateMap(operationRoutesKey).set(entity, verb);
} else {
throwDiagnostic(`HTTP verb already applied to ${entity.name}`, entity);
program.reportDiagnostic(`HTTP verb already applied to ${entity.name}`, entity);
}
} else {
throwDiagnostic(`Cannot use @${verb} on a ${entity.kind}`, entity);
program.reportDiagnostic(`Cannot use @${verb} on a ${entity.kind}`, entity);
}
}
export function getOperationRoute(entity: Type): OperationRoute | undefined {
return operationRoutes.get(entity);
export function getOperationRoute(program: Program, entity: Type): OperationRoute | undefined {
return program.stateMap(operationRoutesKey).get(entity);
}
export function get(program: Program, entity: Type, subPath?: string) {
setOperationRoute(entity, {
setOperationRoute(program, entity, {
verb: "get",
subPath,
});
}
export function put(program: Program, entity: Type, subPath?: string) {
setOperationRoute(entity, {
setOperationRoute(program, entity, {
verb: "put",
subPath,
});
}
export function post(program: Program, entity: Type, subPath?: string) {
setOperationRoute(entity, {
setOperationRoute(program, entity, {
verb: "post",
subPath,
});
}
export function patch(program: Program, entity: Type, subPath?: string) {
setOperationRoute(entity, {
setOperationRoute(program, entity, {
verb: "patch",
subPath,
});
@ -148,7 +149,7 @@ export function patch(program: Program, entity: Type, subPath?: string) {
// BUG #243: How do we deal with reserved words?
export function _delete(program: Program, entity: Type, subPath?: string) {
setOperationRoute(entity, {
setOperationRoute(program, entity, {
verb: "delete",
subPath,
});
@ -156,39 +157,60 @@ export function _delete(program: Program, entity: Type, subPath?: string) {
// -- Service-level Metadata
const serviceDetails: {
interface ServiceDetails {
namespace?: NamespaceType;
title?: string;
version?: string;
host?: string;
} = {};
const programServiceDetails = new WeakMap<Program, ServiceDetails>();
function getServiceDetails(program: Program) {
let serviceDetails = programServiceDetails.get(program);
if (!serviceDetails) {
serviceDetails = {};
programServiceDetails.set(program, serviceDetails);
}
export function _setServiceNamespace(namespace: NamespaceType): void {
return serviceDetails;
}
export function _setServiceNamespace(program: Program, namespace: NamespaceType): void {
const serviceDetails = getServiceDetails(program);
if (serviceDetails.namespace && serviceDetails.namespace !== namespace) {
throwDiagnostic("Cannot set service namespace more than once in an ADL project.", namespace);
program.reportDiagnostic(
"Cannot set service namespace more than once in an ADL project.",
namespace
);
}
serviceDetails.namespace = namespace;
}
export function _checkIfServiceNamespace(namespace: NamespaceType): boolean {
export function _checkIfServiceNamespace(program: Program, namespace: NamespaceType): boolean {
const serviceDetails = getServiceDetails(program);
return serviceDetails.namespace === namespace;
}
export function serviceTitle(program: Program, entity: Type, title: string) {
const serviceDetails = getServiceDetails(program);
if (serviceDetails.title) {
throwDiagnostic("Service title can only be set once per ADL document.", entity);
program.reportDiagnostic("Service title can only be set once per ADL document.", entity);
}
if (entity.kind !== "Namespace") {
throwDiagnostic("The @serviceTitle decorator can only be applied to namespaces.", entity);
program.reportDiagnostic(
"The @serviceTitle decorator can only be applied to namespaces.",
entity
);
return;
}
_setServiceNamespace(entity);
_setServiceNamespace(program, entity);
serviceDetails.title = title;
}
export function getServiceTitle(): string {
export function getServiceTitle(program: Program): string {
const serviceDetails = getServiceDetails(program);
return serviceDetails.title || "(title)";
}
@ -210,56 +232,63 @@ export function getServiceHost(): string {
}
export function serviceVersion(program: Program, entity: Type, version: string) {
const serviceDetails = getServiceDetails(program);
// TODO: This will need to change once we support multiple service versions
if (serviceDetails.version) {
throwDiagnostic("Service version can only be set once per ADL document.", entity);
program.reportDiagnostic("Service version can only be set once per ADL document.", entity);
}
if (entity.kind !== "Namespace") {
throwDiagnostic("The @serviceVersion decorator can only be applied to namespaces.", entity);
program.reportDiagnostic(
"The @serviceVersion decorator can only be applied to namespaces.",
entity
);
return;
}
_setServiceNamespace(entity);
_setServiceNamespace(program, entity);
serviceDetails.version = version;
}
export function getServiceVersion(): string {
export function getServiceVersion(program: Program): string {
const serviceDetails = getServiceDetails(program);
return serviceDetails.version || "0000-00-00";
}
export function getServiceNamespaceString(program: Program): string | undefined {
const serviceDetails = getServiceDetails(program);
return (
(serviceDetails.namespace && program.checker!.getNamespaceString(serviceDetails.namespace)) ||
undefined
);
}
const producesTypes = new Map<Type, string[]>();
const producesTypesKey = Symbol();
export function produces(program: Program, entity: Type, ...contentTypes: string[]) {
if (entity.kind !== "Namespace") {
throwDiagnostic("The @produces decorator can only be applied to namespaces.", entity);
program.reportDiagnostic("The @produces decorator can only be applied to namespaces.", entity);
}
const values = getProduces(entity);
producesTypes.set(entity, values.concat(contentTypes));
const values = getProduces(program, entity);
program.stateMap(producesTypesKey).set(entity, values.concat(contentTypes));
}
export function getProduces(entity: Type): string[] {
return producesTypes.get(entity) || [];
export function getProduces(program: Program, entity: Type): string[] {
return program.stateMap(producesTypesKey).get(entity) || [];
}
const consumesTypes = new Map<Type, string[]>();
const consumesTypesKey = Symbol();
export function consumes(program: Program, entity: Type, ...contentTypes: string[]) {
if (entity.kind !== "Namespace") {
throwDiagnostic("The @consumes decorator can only be applied to namespaces.", entity);
program.reportDiagnostic("The @consumes decorator can only be applied to namespaces.", entity);
}
const values = getConsumes(entity);
consumesTypes.set(entity, values.concat(contentTypes));
const values = getConsumes(program, entity);
program.stateMap(consumesTypesKey).set(entity, values.concat(contentTypes));
}
export function getConsumes(entity: Type): string[] {
return consumesTypes.get(entity) || [];
export function getConsumes(program: Program, entity: Type): string[] {
return program.stateMap(consumesTypesKey).get(entity) || [];
}

Просмотреть файл

@ -1,5 +1,6 @@
{
"extends": "../tsconfig.json",
"references": [{ "path": "../adl/tsconfig.json" }],
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"

Просмотреть файл

@ -1,16 +0,0 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
charset = utf-8
[*.cs]
csharp_new_line_before_open_brace = none
csharp_new_line_before_catch = false
csharp_new_line_before_else = false
csharp_new_line_before_finally = false
csharp_new_line_before_members_in_anonymous_types = false
csharp_new_line_before_members_in_object_initializers = false

Просмотреть файл

@ -1,6 +1,30 @@
{
"name": "@azure-tools/adl-vs",
"entries": [
{
"version": "0.1.5",
"tag": "@azure-tools/adl-vs_v0.1.5",
"date": "Tue, 18 May 2021 23:43:31 GMT",
"comments": {
"dependency": [
{
"comment": "Updating dependency \"adl-vscode\" from `0.5.0` to `0.5.1`"
}
]
}
},
{
"version": "0.1.4",
"tag": "@azure-tools/adl-vs_v0.1.4",
"date": "Thu, 06 May 2021 14:56:02 GMT",
"comments": {
"dependency": [
{
"comment": "Updating dependency \"adl-vscode\" from `0.4.5` to `0.5.0`"
}
]
}
},
{
"version": "0.1.3",
"tag": "@azure-tools/adl-vs_v0.1.3",

Просмотреть файл

@ -1,6 +1,16 @@
# Change Log - @azure-tools/adl-vs
This log was last generated on Tue, 20 Apr 2021 15:23:29 GMT and should not be manually modified.
This log was last generated on Tue, 18 May 2021 23:43:31 GMT and should not be manually modified.
## 0.1.5
Tue, 18 May 2021 23:43:31 GMT
_Version update only_
## 0.1.4
Thu, 06 May 2021 14:56:02 GMT
_Version update only_
## 0.1.3
Tue, 20 Apr 2021 15:23:29 GMT

Просмотреть файл

@ -1,7 +1,7 @@
{
"name": "@azure-tools/adl-vs",
"author": "Microsoft Corporation",
"version": "0.1.3",
"version": "0.1.5",
"description": "ADL Language Support for Visual Studio",
"homepage": "https://github.com/Azure/adl",
"readme": "https://github.com/Azure/adl/blob/master/README.md",
@ -28,6 +28,6 @@
},
"dependencies": {},
"devDependencies": {
"adl-vscode": "0.4.5"
"adl-vscode": "0.5.1"
}
}

Просмотреть файл

@ -40,13 +40,8 @@ if (proc.status != 0 || proc.error || !proc.stdout) {
// In official build on Windows, it's an error if VS is not found.
console.error(`error: ${message}`);
process.exit(1);
} else if (proc.error?.code === "ENOENT") {
// If developer has no version of VS installed, skip build without warning.
console.log(`Skipping adl-vs build: ${message}.`);
process.exit(0);
} else {
// If developer has VS but it's not recent enough, skip build with warning.
console.error(`warning: ${message}. Skipping adl-vs build.`);
console.log(`Skipping adl-vs build: ${message}.`);
process.exit(0);
}
}

Просмотреть файл

@ -1,6 +1,35 @@
{
"name": "adl-vscode",
"entries": [
{
"version": "0.5.1",
"tag": "adl-vscode_v0.5.1",
"date": "Tue, 18 May 2021 23:43:31 GMT",
"comments": {
"patch": [
{
"comment": "Fix issue launching adl-server on Mac OS"
}
]
}
},
{
"version": "0.5.0",
"tag": "adl-vscode_v0.5.0",
"date": "Thu, 06 May 2021 14:56:02 GMT",
"comments": {
"minor": [
{
"comment": "Implement alias and enum, remove model ="
}
],
"patch": [
{
"comment": "Update syntax highlighting for string literal change"
}
]
}
},
{
"version": "0.4.5",
"tag": "adl-vscode_v0.4.5",

Просмотреть файл

@ -1,6 +1,24 @@
# Change Log - adl-vscode
This log was last generated on Tue, 20 Apr 2021 15:23:29 GMT and should not be manually modified.
This log was last generated on Tue, 18 May 2021 23:43:31 GMT and should not be manually modified.
## 0.5.1
Tue, 18 May 2021 23:43:31 GMT
### Patches
- Fix issue launching adl-server on Mac OS
## 0.5.0
Thu, 06 May 2021 14:56:02 GMT
### Minor changes
- Implement alias and enum, remove model =
### Patches
- Update syntax highlighting for string literal change
## 0.4.5
Tue, 20 Apr 2021 15:23:29 GMT

Просмотреть файл

@ -1,6 +1,6 @@
{
"name": "adl-vscode",
"version": "0.4.5",
"version": "0.5.1",
"author": "Microsoft Corporation",
"description": "ADL Language Support for VS Code",
"homepage": "https://github.com/Azure/adl",
@ -75,7 +75,7 @@
"watch-tmlanguage": "node scripts/watch-tmlanguage.js",
"dogfood": "node scripts/dogfood.js",
"generate-tmlanguage": "node scripts/generate-tmlanguage.js",
"generate-third-party-notices": "node scripts/generate-third-party-notices.js",
"generate-third-party-notices": "node ../../eng/scripts/generate-third-party-notices",
"rollup": "rollup --config --failAfterWarnings 2>&1",
"package-vsix": "vsce package --yarn"
},
@ -84,10 +84,12 @@
"@azure-tools/tmlanguage-generator": "0.1.4",
"@rollup/plugin-commonjs": "~17.1.0",
"@rollup/plugin-node-resolve": "~11.2.0",
"@types/mkdirp": "~1.0.1",
"@types/node": "~14.0.27",
"@types/vscode": "~1.53.0",
"mkdirp": "~1.0.4",
"rollup": "~2.41.4",
"typescript": "~4.2.4",
"typescript": "~4.3.2",
"vsce": "~1.85.1",
"vscode-languageclient": "~7.0.0",
"watch": "~1.0.2"

Просмотреть файл

@ -1,12 +1,20 @@
import { ExtensionContext, workspace } from "vscode";
import { Executable, LanguageClient, LanguageClientOptions } from "vscode-languageclient/node.js";
import {
Executable,
ExecutableOptions,
LanguageClient,
LanguageClientOptions,
} from "vscode-languageclient/node.js";
let client: LanguageClient | undefined;
export function activate(context: ExtensionContext) {
const exe = resolveADLServer(context);
const options: LanguageClientOptions = {
documentSelector: [{ scheme: "file", language: "adl" }],
documentSelector: [
{ scheme: "file", language: "adl" },
{ scheme: "untitled", language: "adl" },
],
};
const name = "ADL";
@ -19,9 +27,9 @@ function resolveADLServer(context: ExtensionContext): Executable {
const nodeOptions = process.env.ADL_SERVER_NODE_OPTIONS;
const args = ["--stdio"];
// In development mode (F5 launch from source), resolve to locally built adl-server.js.
// In development mode (F5 launch from source), resolve to locally built server.js.
if (process.env.ADL_DEVELOPMENT_MODE) {
const script = context.asAbsolutePath("../adl/cmd/adl-server.js");
const script = context.asAbsolutePath("../adl/dist/server/server.js");
// we use CLI instead of NODE_OPTIONS environment variable in this case
// because --nolazy is not supported by NODE_OPTIONS.
const options = nodeOptions?.split(" ") ?? [];
@ -50,7 +58,14 @@ function resolveADLServer(context: ExtensionContext): Executable {
command += ".cmd";
}
return { command, args, options: { env: { NODE_OPTIONS: nodeOptions } } };
let options: ExecutableOptions | undefined;
if (nodeOptions) {
options = {
env: { ...process.env, NODE_OPTIONS: nodeOptions },
};
}
return { command, args, options };
}
export async function deactivate() {

Просмотреть файл

@ -3,6 +3,7 @@
import * as tm from "@azure-tools/tmlanguage-generator";
import fs from "fs/promises";
import mkdirp from "mkdirp";
import { resolve } from "path";
type IncludeRule = tm.IncludeRule<ADLScope>;
@ -20,6 +21,7 @@ type ADLScope =
| "entity.name.function.adl"
| "keyword.other.adl"
| "string.quoted.double.adl"
| "string.quoted.triple.adl"
| "variable.name.adl";
const meta: typeof tm.meta = tm.meta;
@ -28,7 +30,7 @@ const identifierContinue = "[_$[:alnum:]]";
const beforeIdentifier = `(?=${identifierStart})`;
const identifier = `\\b${identifierStart}${identifierContinue}*\\b`;
const stringPattern = '\\"(?:[^\\"\\\\]|\\\\.)*\\"';
const statementKeyword = `\\b(?:namespace|model|op|using|import)\\b`;
const statementKeyword = `\\b(?:namespace|model|op|using|import|enum|alias)\\b`;
const universalEnd = `(?=,|;|@|\\)|\\}|${statementKeyword})`;
const hexNumber = "\\b(?<!\\$)0(?:x|X)[0-9a-fA-F][0-9a-fA-F_]*(n)?\\b(?!\\$)";
const binaryNumber = "\\b(?<!\\$)0(?:b|B)[01][01_]*(n)?\\b(?!\\$)";
@ -71,13 +73,19 @@ const escapeChar: MatchRule = {
match: "\\\\.",
};
// TODO: Triple-quoted """X""" currently matches as three string literals
// ("" "X" "") but should be its own thing.
const stringLiteral: BeginEndRule = {
key: "string-literal",
scope: "string.quoted.double.adl",
begin: '"',
end: '"',
end: '"|$',
patterns: [escapeChar],
};
const tripleQuotedStringLiteral: BeginEndRule = {
key: "triple-quoted-string-literal",
scope: "string.quoted.triple.adl",
begin: '"""',
end: '"""',
patterns: [escapeChar],
};
@ -103,7 +111,16 @@ const blockComment: BeginEndRule = {
// Tokens that match standing alone in any context: literals and comments
const token: IncludeRule = {
key: "token",
patterns: [lineComment, blockComment, stringLiteral, booleanLiteral, numericLiteral],
patterns: [
lineComment,
blockComment,
// `"""` must come before `"` or first two quotes of `"""` will match as
// empty string
tripleQuotedStringLiteral,
stringLiteral,
booleanLiteral,
numericLiteral,
],
};
const parenthesizedExpression: BeginEndRule = {
@ -180,7 +197,15 @@ const modelExpression: BeginEndRule = {
scope: meta,
begin: "\\{",
end: "\\}",
patterns: [token, decorator, modelProperty, modelSpreadProperty],
patterns: [
// modelProperty must come before token or quoted property name will be
// considered an arbitrarily positioned string literal and not match as part
// of modelProperty begin.
modelProperty,
token,
decorator,
modelSpreadProperty,
],
};
const modelHeritage: BeginEndRule = {
@ -209,6 +234,28 @@ const modelStatement: BeginEndRule = {
],
};
const enumStatement: BeginEndRule = {
key: "enum-statement",
scope: meta,
begin: "\\b(enum)\\b",
beginCaptures: {
"1": { scope: "keyword.other.adl" },
},
end: `(?<=\\})|${universalEnd}`,
patterns: [token, expression],
};
const aliasStatement: BeginEndRule = {
key: "alias-statement",
scope: meta,
begin: "\\b(alias)\\b",
beginCaptures: {
"1": { scope: "keyword.other.adl" },
},
end: universalEnd,
patterns: [token, expression],
};
const namespaceName: BeginEndRule = {
key: "namespace-name",
scope: meta,
@ -315,6 +362,8 @@ statement.patterns = [
token,
decorator,
modelStatement,
enumStatement,
aliasStatement,
namespaceStatement,
operationStatement,
importStatement,
@ -333,5 +382,6 @@ export async function main() {
const plist = await tm.emitPList(grammar, {
errorSourceFilePath: resolve("./src/tmlanguage.ts"),
});
await mkdirp("./dist");
await fs.writeFile("./dist/adl.tmLanguage", plist);
}

Просмотреть файл

@ -1,5 +1,6 @@
{
"extends": "../tsconfig.json",
"references": [{ "path": "../tmlanguage-generator/tsconfig.json" }],
"compilerOptions": {
"outDir": "dist-dev",
"rootDir": "src",

Просмотреть файл

@ -1,6 +1,85 @@
{
"name": "@azure-tools/adl",
"entries": [
{
"version": "0.11.0",
"tag": "@azure-tools/adl_v0.11.0",
"date": "Tue, 18 May 2021 23:43:31 GMT",
"comments": {
"patch": [
{
"comment": "**Fix** Throw diagnostic when main ADL file doesn't exists"
},
{
"comment": "Fix TypeError after `adl vs` command"
},
{
"comment": "**Fix** formatter handling string and number literal should keep as it is."
},
{
"comment": "**Fix** Formatter not rendering template parameters of models."
},
{
"comment": "**Fix** Don't format a file with parsing errors"
},
{
"comment": "Work around npm 7+ Mac OS bug in `adl code install`"
},
{
"comment": "Prefer local install of adl package when running global `adl`"
}
],
"minor": [
{
"comment": "**Added** format command to automatically format adl files"
}
]
}
},
{
"version": "0.10.0",
"tag": "@azure-tools/adl_v0.10.0",
"date": "Thu, 06 May 2021 14:56:02 GMT",
"comments": {
"minor": [
{
"comment": "Implement alias and enum, remove model ="
},
{
"comment": "Implement basic parser error recovery"
},
{
"comment": "Add API to check if a node or any descendants have parse errors"
}
],
"patch": [
{
"comment": "Small parsing speed improvement when expecting one of N tokens."
},
{
"comment": "Fix blockless namespaces not accumulating decls"
},
{
"comment": "Allow leading +/- in numeric literals and require fractional digits"
},
{
"comment": "Fix bugs with non-ascii identifiers"
},
{
"comment": "Improve CLI experience for generate command"
},
{
"comment": "Replace several internal compiler errors with diagnostics"
},
{
"comment": "Do not allow multi-line non-triple-quoted string literals"
},
{
"comment": "Fix parsing edge cases and optimize parsing slightly"
}
]
}
},
{
"version": "0.9.0",
"tag": "@azure-tools/adl_v0.9.0",

Просмотреть файл

@ -1,6 +1,43 @@
# Change Log - @azure-tools/adl
This log was last generated on Tue, 20 Apr 2021 15:23:29 GMT and should not be manually modified.
This log was last generated on Tue, 18 May 2021 23:43:31 GMT and should not be manually modified.
## 0.11.0
Tue, 18 May 2021 23:43:31 GMT
### Minor changes
- **Added** format command to automatically format adl files
### Patches
- **Fix** Throw diagnostic when main ADL file doesn't exists
- Fix TypeError after `adl vs` command
- **Fix** formatter handling string and number literal should keep as it is.
- **Fix** Formatter not rendering template parameters of models.
- **Fix** Don't format a file with parsing errors
- Work around npm 7+ Mac OS bug in `adl code install`
- Prefer local install of adl package when running global `adl`
## 0.10.0
Thu, 06 May 2021 14:56:02 GMT
### Minor changes
- Implement alias and enum, remove model =
- Implement basic parser error recovery
- Add API to check if a node or any descendants have parse errors
### Patches
- Small parsing speed improvement when expecting one of N tokens.
- Fix blockless namespaces not accumulating decls
- Allow leading +/- in numeric literals and require fractional digits
- Fix bugs with non-ascii identifiers
- Improve CLI experience for generate command
- Replace several internal compiler errors with diagnostics
- Do not allow multi-line non-triple-quoted string literals
- Fix parsing edge cases and optimize parsing slightly
## 0.9.0
Tue, 20 Apr 2021 15:23:29 GMT

Просмотреть файл

@ -1,2 +1,3 @@
#!/usr/bin/env node
await import("../dist/server/server.js");
import { runScript } from "../dist/cmd/runner.js";
await runScript("dist/server/server.js");

Просмотреть файл

@ -1,2 +1,3 @@
#!/usr/bin/env node
await import("../dist/compiler/cli.js");
import { runScript } from "../dist/cmd/runner.js";
await runScript("dist/compiler/cli.js");

Просмотреть файл

@ -0,0 +1,43 @@
import path from "path";
import resolveModule from "resolve";
import url from "url";
/**
* Run script given by relative path from @azure-tools/adl package root.
* Prefer local install resolved from cwd over current package.
*
* Prevents loading two conflicting copies of ADL modules from global and
* local package locations.
*/
export function runScript(relativePath: string): Promise<void> {
return new Promise((resolve, reject) => {
resolveModule(
"@azure-tools/adl",
{
basedir: process.cwd(),
preserveSymlinks: false,
},
(err, resolved) => {
let packageRoot: string;
if (err) {
if ((err as any).code === "MODULE_NOT_FOUND") {
// Resolution from cwd failed: use current package.
packageRoot = path.resolve(url.fileURLToPath(import.meta.url), "../../..");
} else {
reject(err);
return;
}
} else if (!resolved) {
reject(new Error("BUG: Module resolution succeeded, but didn't return a value."));
return;
} else {
// Resolution succeeded to dist/compiler/index.js in local package.
packageRoot = path.resolve(resolved, "../../..");
}
const script = path.join(packageRoot, relativePath);
const scriptUrl = url.pathToFileURL(script).toString();
resolve(import(scriptUrl));
}
);
});
}

Просмотреть файл

@ -3,7 +3,9 @@ import { visitChildren } from "./parser.js";
import { Program } from "./program.js";
import {
ADLScriptNode,
AliasStatementNode,
Declaration,
EnumStatementNode,
ModelStatementNode,
NamespaceStatementNode,
Node,
@ -15,7 +17,6 @@ import {
TemplateParameterDeclarationNode,
UsingStatementNode,
} from "./types.js";
import { reportDuplicateSymbols } from "./util.js";
const SymbolTable = class extends Map<string, Sym> implements SymbolTable {
duplicates = new Set<Sym>();
@ -47,16 +48,23 @@ export interface TypeSymbol {
}
export interface Binder {
bindSourceFile(program: Program, sourceFile: ADLScriptNode): void;
bindSourceFile(sourceFile: ADLScriptNode): void;
bindNode(node: Node): void;
}
export function createSymbolTable(): SymbolTable {
return new SymbolTable();
}
export function createBinder(): Binder {
export interface BinderOptions {
// Configures the initial parent node to use when calling bindNode. This is
// useful for binding ADL fragments outside the context of a full script node.
initialParentNode?: Node;
}
export function createBinder(program: Program, options: BinderOptions = {}): Binder {
let currentFile: ADLScriptNode;
let parentNode: Node;
let parentNode: Node | undefined = options?.initialParentNode;
let globalNamespace: NamespaceStatementNode;
let fileNamespace: NamespaceStatementNode;
let currentNamespace: NamespaceStatementNode;
@ -65,9 +73,10 @@ export function createBinder(): Binder {
let scope: ScopeNode;
return {
bindSourceFile,
bindNode,
};
function bindSourceFile(program: Program, sourceFile: ADLScriptNode) {
function bindSourceFile(sourceFile: ADLScriptNode) {
globalNamespace = program.globalNamespace;
fileNamespace = globalNamespace;
currentFile = sourceFile;
@ -85,6 +94,12 @@ export function createBinder(): Binder {
case SyntaxKind.ModelStatement:
bindModelStatement(node);
break;
case SyntaxKind.AliasStatement:
bindAliasStatement(node);
break;
case SyntaxKind.EnumStatement:
bindEnumStatement(node);
break;
case SyntaxKind.NamespaceStatement:
bindNamespaceStatement(node);
break;
@ -114,7 +129,7 @@ export function createBinder(): Binder {
visitChildren(node, bindNode);
if (node.kind !== SyntaxKind.NamespaceStatement) {
reportDuplicateSymbols(node.locals!);
program.reportDuplicateSymbols(node.locals!);
}
scope = prevScope;
@ -148,6 +163,16 @@ export function createBinder(): Binder {
node.locals = new SymbolTable();
}
function bindAliasStatement(node: AliasStatementNode) {
declareSymbol(getContainingSymbolTable(), node, node.id.sv);
// Initialize locals for type parameters
node.locals = new SymbolTable();
}
function bindEnumStatement(node: EnumStatementNode) {
declareSymbol(getContainingSymbolTable(), node, node.id.sv);
}
function bindNamespaceStatement(statement: NamespaceStatementNode) {
// check if there's an existing symbol for this namespace
const existingBinding = currentNamespace.exports!.get(statement.name.sv);
@ -217,6 +242,7 @@ export function createBinder(): Binder {
function hasScope(node: Node): node is ScopeNode {
switch (node.kind) {
case SyntaxKind.ModelStatement:
case SyntaxKind.AliasStatement:
return true;
case SyntaxKind.NamespaceStatement:
return node.statements !== undefined;

Просмотреть файл

@ -1,247 +0,0 @@
import { nonAsciiIdentifierContinueMap, nonAsciiIdentifierStartMap } from "./non-ascii-maps.js";
export const enum CharacterCodes {
nullCharacter = 0,
maxAsciiCharacter = 0x7f,
lineFeed = 0x0a,
carriageReturn = 0x0d,
lineSeparator = 0x2028,
paragraphSeparator = 0x2029,
nextLine = 0x0085,
// Unicode 3.0 space characters
space = 0x0020,
nonBreakingSpace = 0x00a0,
enQuad = 0x2000,
emQuad = 0x2001,
enSpace = 0x2002,
emSpace = 0x2003,
threePerEmSpace = 0x2004,
fourPerEmSpace = 0x2005,
sixPerEmSpace = 0x2006,
figureSpace = 0x2007,
punctuationSpace = 0x2008,
thinSpace = 0x2009,
hairSpace = 0x200a,
zeroWidthSpace = 0x200b,
narrowNoBreakSpace = 0x202f,
ideographicSpace = 0x3000,
mathematicalSpace = 0x205f,
ogham = 0x1680,
_ = 0x5f,
$ = 0x24,
_0 = 0x30,
_1 = 0x31,
_2 = 0x32,
_3 = 0x33,
_4 = 0x34,
_5 = 0x35,
_6 = 0x36,
_7 = 0x37,
_8 = 0x38,
_9 = 0x39,
a = 0x61,
b = 0x62,
c = 0x63,
d = 0x64,
e = 0x65,
f = 0x66,
g = 0x67,
h = 0x68,
i = 0x69,
j = 0x6a,
k = 0x6b,
l = 0x6c,
m = 0x6d,
n = 0x6e,
o = 0x6f,
p = 0x70,
q = 0x71,
r = 0x72,
s = 0x73,
t = 0x74,
u = 0x75,
v = 0x76,
w = 0x77,
x = 0x78,
y = 0x79,
z = 0x7a,
A = 0x41,
B = 0x42,
C = 0x43,
D = 0x44,
E = 0x45,
F = 0x46,
G = 0x47,
H = 0x48,
I = 0x49,
J = 0x4a,
K = 0x4b,
L = 0x4c,
M = 0x4d,
N = 0x4e,
O = 0x4f,
P = 0x50,
Q = 0x51,
R = 0x52,
S = 0x53,
T = 0x54,
U = 0x55,
V = 0x56,
W = 0x57,
X = 0x58,
Y = 0x59,
Z = 0x5a,
ampersand = 0x26,
asterisk = 0x2a,
at = 0x40,
backslash = 0x5c,
backtick = 0x60,
bar = 0x7c,
caret = 0x5e,
closeBrace = 0x7d,
closeBracket = 0x5d,
closeParen = 0x29,
colon = 0x3a,
comma = 0x2c,
dot = 0x2e,
doubleQuote = 0x22,
equals = 0x3d,
exclamation = 0x21,
greaterThan = 0x3e,
hash = 0x23,
lessThan = 0x3c,
minus = 0x2d,
openBrace = 0x7b,
openBracket = 0x5b,
openParen = 0x28,
percent = 0x25,
plus = 0x2b,
question = 0x3f,
semicolon = 0x3b,
singleQuote = 0x27,
slash = 0x2f,
tilde = 0x7e,
backspace = 0x08,
formFeed = 0x0c,
byteOrderMark = 0xfeff,
tab = 0x09,
verticalTab = 0x0b,
}
/** Does not include line breaks. For that, see isWhiteSpaceLike. */
export function isWhiteSpaceSingleLine(ch: number): boolean {
// Note: nextLine is in the Zs space, and should be considered to be a whitespace.
// It is explicitly not a line-break as it isn't in the exact set specified by EcmaScript.
return (
ch === CharacterCodes.space ||
ch === CharacterCodes.tab ||
ch === CharacterCodes.verticalTab ||
ch === CharacterCodes.formFeed ||
ch === CharacterCodes.nonBreakingSpace ||
ch === CharacterCodes.nextLine ||
ch === CharacterCodes.ogham ||
(ch >= CharacterCodes.enQuad && ch <= CharacterCodes.zeroWidthSpace) ||
ch === CharacterCodes.narrowNoBreakSpace ||
ch === CharacterCodes.mathematicalSpace ||
ch === CharacterCodes.ideographicSpace ||
ch === CharacterCodes.byteOrderMark
);
}
export function isLineBreak(ch: number): boolean {
// Other new line or line
// breaking characters are treated as white space but not as line terminators.
return (
ch === CharacterCodes.lineFeed ||
ch === CharacterCodes.carriageReturn ||
ch === CharacterCodes.lineSeparator ||
ch === CharacterCodes.paragraphSeparator
);
}
export function isDigit(ch: number): boolean {
return ch >= CharacterCodes._0 && ch <= CharacterCodes._9;
}
export function isHexDigit(ch: number): boolean {
return (
isDigit(ch) ||
(ch >= CharacterCodes.A && ch <= CharacterCodes.F) ||
(ch >= CharacterCodes.a && ch <= CharacterCodes.f)
);
}
export function isBinaryDigit(ch: number): boolean {
return ch === CharacterCodes._0 || ch === CharacterCodes._1;
}
export function isAsciiIdentifierStart(ch: number): boolean {
return (
(ch >= CharacterCodes.A && ch <= CharacterCodes.Z) ||
(ch >= CharacterCodes.a && ch <= CharacterCodes.z) ||
ch === CharacterCodes.$ ||
ch === CharacterCodes._
);
}
export function isAsciiIdentifierContinue(ch: number): boolean {
return (
(ch >= CharacterCodes.A && ch <= CharacterCodes.Z) ||
(ch >= CharacterCodes.a && ch <= CharacterCodes.z) ||
(ch >= CharacterCodes._0 && ch <= CharacterCodes._9) ||
ch === CharacterCodes.$ ||
ch === CharacterCodes._
);
}
export function isIdentifierContinue(codePoint: number) {
return (
isAsciiIdentifierStart(codePoint) ||
(codePoint > CharacterCodes.maxAsciiCharacter && isNonAsciiIdentifierContinue(codePoint))
);
}
export function isNonAsciiIdentifierStart(codePoint: number) {
return lookupInNonAsciiMap(codePoint, nonAsciiIdentifierStartMap);
}
export function isNonAsciiIdentifierContinue(codePoint: number) {
return lookupInNonAsciiMap(codePoint, nonAsciiIdentifierContinueMap);
}
function lookupInNonAsciiMap(codePoint: number, map: readonly number[]): boolean {
// Bail out quickly if it couldn't possibly be in the map.
if (codePoint < map[0]) {
return false;
}
// Perform binary search in one of the Unicode range maps
let lo = 0;
let hi: number = map.length;
let mid: number;
while (lo + 1 < hi) {
mid = lo + (hi - lo) / 2;
// mid has to be even to catch a range's beginning
mid -= mid % 2;
if (map[mid] <= codePoint && codePoint <= map[mid + 1]) {
return true;
}
if (codePoint < map[mid]) {
hi = mid;
} else {
lo = mid + 2;
}
}
return false;
}

Просмотреть файл

@ -0,0 +1,283 @@
import { nonAsciiIdentifierContinueMap, nonAsciiIdentifierStartMap } from "./nonascii.js";
export const enum CharCode {
Null = 0x00,
MaxAscii = 0x7f,
// ASCII line breaks
LineFeed = 0x0a,
CarriageReturn = 0x0d,
// Non-ASCII line breaks
LineSeparator = 0x2028,
ParagraphSeparator = 0x2029,
// ASCII whitespace excluding line breaks
Space = 0x20,
FormFeed = 0x0c,
Tab = 0x09,
VerticalTab = 0x0b,
// Non-ASCII whitespace excluding line breaks
ByteOrderMark = 0xfeff, // currently allowed anywhere
NextLine = 0x0085, // not considered a line break, mirroring ECMA-262
NonBreakingSpace = 0x00a0,
EnQuad = 0x2000,
EmQuad = 0x2001,
EnSpace = 0x2002,
EmSpace = 0x2003,
ThreePerEmSpace = 0x2004,
FourPerEmSpace = 0x2005,
SixPerEmSpace = 0x2006,
FigureSpace = 0x2007,
PunctuationSpace = 0x2008,
ThinSpace = 0x2009,
HairSpace = 0x200a,
ZeroWidthSpace = 0x200b,
NarrowNoBreakSpace = 0x202f,
IdeographicSpace = 0x3000,
MathematicalSpace = 0x205f,
Ogham = 0x1680,
// ASCII Digits
_0 = 0x30,
_1 = 0x31,
_2 = 0x32,
_3 = 0x33,
_4 = 0x34,
_5 = 0x35,
_6 = 0x36,
_7 = 0x37,
_8 = 0x38,
_9 = 0x39,
// ASCII lowercase letters
a = 0x61,
b = 0x62,
c = 0x63,
d = 0x64,
e = 0x65,
f = 0x66,
g = 0x67,
h = 0x68,
i = 0x69,
j = 0x6a,
k = 0x6b,
l = 0x6c,
m = 0x6d,
n = 0x6e,
o = 0x6f,
p = 0x70,
q = 0x71,
r = 0x72,
s = 0x73,
t = 0x74,
u = 0x75,
v = 0x76,
w = 0x77,
x = 0x78,
y = 0x79,
z = 0x7a,
// ASCII uppercase letters
A = 0x41,
B = 0x42,
C = 0x43,
D = 0x44,
E = 0x45,
F = 0x46,
G = 0x47,
H = 0x48,
I = 0x49,
J = 0x4a,
K = 0x4b,
L = 0x4c,
M = 0x4d,
N = 0x4e,
O = 0x4f,
P = 0x50,
Q = 0x51,
R = 0x52,
S = 0x53,
T = 0x54,
U = 0x55,
V = 0x56,
W = 0x57,
X = 0x58,
Y = 0x59,
Z = 0x5a,
// Non-letter, non-digit ASCII characters that are valid in identifiers
_ = 0x5f,
$ = 0x24,
// ASCII punctuation
Ampersand = 0x26,
Asterisk = 0x2a,
At = 0x40,
Backslash = 0x5c,
Backtick = 0x60,
Bar = 0x7c,
Caret = 0x5e,
CloseBrace = 0x7d,
CloseBracket = 0x5d,
CloseParen = 0x29,
Colon = 0x3a,
Comma = 0x2c,
Dot = 0x2e,
DoubleQuote = 0x22,
Equals = 0x3d,
Exclamation = 0x21,
GreaterThan = 0x3e,
Hash = 0x23,
LessThan = 0x3c,
Minus = 0x2d,
OpenBrace = 0x7b,
OpenBracket = 0x5b,
OpenParen = 0x28,
Percent = 0x25,
Plus = 0x2b,
Question = 0x3f,
Semicolon = 0x3b,
SingleQuote = 0x27,
Slash = 0x2f,
Tilde = 0x7e,
}
export function utf16CodeUnits(codePoint: number) {
return codePoint >= 0x10000 ? 2 : 1;
}
export function isAsciiLineBreak(ch: number) {
return ch === CharCode.LineFeed || ch == CharCode.CarriageReturn;
}
export function isAsciiWhiteSpaceSingleLine(ch: number) {
return (
ch === CharCode.Space ||
ch === CharCode.Tab ||
ch === CharCode.VerticalTab ||
ch === CharCode.FormFeed
);
}
export function isNonAsciiWhiteSpaceSingleLine(ch: number) {
// Note: nextLine is in the Zs space, and should be considered to be a
// whitespace. It is explicitly not a line-break as it isn't in the exact set
// inherited by ADL from JavaScript.
return (
ch === CharCode.NonBreakingSpace ||
ch === CharCode.NextLine ||
ch === CharCode.Ogham ||
(ch >= CharCode.EnQuad && ch <= CharCode.ZeroWidthSpace) ||
ch === CharCode.NarrowNoBreakSpace ||
ch === CharCode.MathematicalSpace ||
ch === CharCode.IdeographicSpace ||
ch === CharCode.ByteOrderMark
);
}
export function isNonAsciiLineBreak(ch: number) {
// Other new line or line breaking characters are treated as white space but
// not as line terminators.
return ch === CharCode.ParagraphSeparator || ch === CharCode.LineSeparator;
}
export function isWhiteSpaceSingleLine(ch: number) {
return (
isAsciiWhiteSpaceSingleLine(ch) ||
(ch > CharCode.MaxAscii && isNonAsciiWhiteSpaceSingleLine(ch))
);
}
export function isLineBreak(ch: number) {
return isAsciiLineBreak(ch) || (ch > CharCode.MaxAscii && isNonAsciiLineBreak(ch));
}
export function isDigit(ch: number) {
return ch >= CharCode._0 && ch <= CharCode._9;
}
export function isHexDigit(ch: number) {
return (
isDigit(ch) || (ch >= CharCode.A && ch <= CharCode.F) || (ch >= CharCode.a && ch <= CharCode.f)
);
}
export function isBinaryDigit(ch: number) {
return ch === CharCode._0 || ch === CharCode._1;
}
export function isLowercaseAsciiLetter(ch: number) {
return ch >= CharCode.a && ch <= CharCode.z;
}
export function isAsciiIdentifierStart(ch: number) {
return (
(ch >= CharCode.A && ch <= CharCode.Z) ||
(ch >= CharCode.a && ch <= CharCode.z) ||
ch === CharCode.$ ||
ch === CharCode._
);
}
export function isAsciiIdentifierContinue(ch: number) {
return (
(ch >= CharCode.A && ch <= CharCode.Z) ||
(ch >= CharCode.a && ch <= CharCode.z) ||
(ch >= CharCode._0 && ch <= CharCode._9) ||
ch === CharCode.$ ||
ch === CharCode._
);
}
export function isIdentifierStart(codePoint: number) {
return (
isAsciiIdentifierStart(codePoint) ||
(codePoint > CharCode.MaxAscii && isNonAsciiIdentifierStart(codePoint))
);
}
export function isIdentifierContinue(codePoint: number) {
return (
isAsciiIdentifierContinue(codePoint) ||
(codePoint > CharCode.MaxAscii && isNonAsciiIdentifierContinue(codePoint))
);
}
export function isNonAsciiIdentifierStart(codePoint: number) {
return lookupInNonAsciiMap(codePoint, nonAsciiIdentifierStartMap);
}
export function isNonAsciiIdentifierContinue(codePoint: number) {
return lookupInNonAsciiMap(codePoint, nonAsciiIdentifierContinueMap);
}
function lookupInNonAsciiMap(codePoint: number, map: readonly number[]) {
// Bail out quickly if it couldn't possibly be in the map.
if (codePoint < map[0]) {
return false;
}
// Perform binary search in one of the Unicode range maps
let lo = 0;
let hi: number = map.length;
let mid: number;
while (lo + 1 < hi) {
mid = lo + (hi - lo) / 2;
// mid has to be even to catch a range's beginning
mid -= mid % 2;
if (map[mid] <= codePoint && codePoint <= map[mid + 1]) {
return true;
}
if (codePoint < map[mid]) {
hi = mid;
} else {
lo = mid + 2;
}
}
return false;
}

Просмотреть файл

@ -1,14 +1,20 @@
import { compilerAssert, throwDiagnostic } from "./diagnostics.js";
import { compilerAssert } from "./diagnostics.js";
import { hasParseError } from "./parser.js";
import { Program } from "./program.js";
import {
ADLScriptNode,
AliasStatementNode,
ArrayExpressionNode,
BooleanLiteralNode,
BooleanLiteralType,
DecoratorSymbol,
EnumMemberNode,
EnumMemberType,
EnumStatementNode,
EnumType,
ErrorType,
IdentifierNode,
IntersectionExpressionNode,
IntrinsicType,
LiteralNode,
LiteralType,
ModelExpressionNode,
@ -39,7 +45,22 @@ import {
UnionExpressionNode,
UnionType,
} from "./types.js";
import { reportDuplicateSymbols } from "./util.js";
export interface Checker {
getTypeForNode(node: Node): Type;
checkProgram(program: Program): void;
checkModelProperty(prop: ModelPropertyNode): ModelTypeProperty;
checkUnionExpression(node: UnionExpressionNode): UnionType;
getGlobalNamespaceType(): NamespaceType;
getLiteralType(node: StringLiteralNode): StringLiteralType;
getLiteralType(node: NumericLiteralNode): NumericLiteralType;
getLiteralType(node: BooleanLiteralNode): BooleanLiteralType;
getLiteralType(node: LiteralNode): LiteralType;
getTypeName(type: Type): string;
getNamespaceString(type: NamespaceType | undefined): string;
}
/**
* A map keyed by a set of objects.
@ -87,12 +108,21 @@ interface PendingModelInfo {
type: ModelType;
}
export function createChecker(program: Program) {
export function createChecker(program: Program): Checker {
let templateInstantiation: Type[] = [];
let instantiatingTemplate: Node | undefined;
let currentSymbolId = 0;
const symbolLinks = new Map<number, SymbolLinks>();
const errorType: IntrinsicType = { kind: "Intrinsic", name: "ErrorType" };
const root = createType({
kind: "Namespace",
name: "",
node: program.globalNamespace,
models: new Map(),
operations: new Map(),
namespaces: new Map(),
});
const errorType: ErrorType = { kind: "Intrinsic", name: "ErrorType" };
// This variable holds on to the model type that is currently
// being instantiated in checkModelStatement so that it is
@ -103,9 +133,17 @@ export function createChecker(program: Program) {
for (const using of file.usings) {
const parentNs = using.parent! as NamespaceStatementNode | ADLScriptNode;
const sym = resolveTypeReference(using.name);
if (sym.kind === "decorator") throwDiagnostic("Can't use a decorator", using);
if (!sym) {
continue;
}
if (sym.kind === "decorator") {
program.reportDiagnostic("Can't use a decorator", using);
continue;
}
if (sym.node.kind !== SyntaxKind.NamespaceStatement) {
throwDiagnostic("Using must refer to a namespace", using);
program.reportDiagnostic("Using must refer to a namespace", using);
continue;
}
for (const [name, binding] of sym.node.exports!) {
@ -117,10 +155,12 @@ export function createChecker(program: Program) {
return {
getTypeForNode,
checkProgram,
checkModelProperty,
checkUnionExpression,
getLiteralType,
getTypeName,
getNamespaceString,
checkOperation,
getGlobalNamespaceType,
};
function getTypeForNode(node: Node): Type {
@ -131,6 +171,10 @@ export function createChecker(program: Program) {
return checkModel(node);
case SyntaxKind.ModelProperty:
return checkModelProperty(node);
case SyntaxKind.AliasStatement:
return checkAlias(node);
case SyntaxKind.EnumStatement:
return checkEnum(node);
case SyntaxKind.NamespaceStatement:
return checkNamespace(node);
case SyntaxKind.OperationStatement:
@ -162,6 +206,8 @@ export function createChecker(program: Program) {
switch (type.kind) {
case "Model":
return getModelName(type);
case "Enum":
return getEnumName(type);
case "Union":
return type.options.map(getTypeName).join(" | ");
case "Array":
@ -178,8 +224,12 @@ export function createChecker(program: Program) {
function getNamespaceString(type: NamespaceType | undefined): string {
if (!type) return "";
const parent = type.namespace;
return parent && parent.name !== "" ? `${getNamespaceString(parent)}.${type.name}` : type.name;
}
return parent ? `${getNamespaceString(parent)}.${type.name}` : type.name;
function getEnumName(e: EnumType): string {
const nsName = getNamespaceString(e.namespace);
return nsName ? `${nsName}.${e.name}` : e.name;
}
function getModelName(model: ModelType) {
@ -215,18 +265,29 @@ export function createChecker(program: Program) {
function checkTypeReference(node: TypeReferenceNode): Type {
const sym = resolveTypeReference(node);
if (!sym) {
return errorType;
}
if (sym.kind === "decorator") {
throwDiagnostic("Can't put a decorator in a type", node);
program.reportDiagnostic("Can't put a decorator in a type", node);
return errorType;
}
const symbolLinks = getSymbolLinks(sym);
const args = node.arguments.map(getTypeForNode);
let args = node.arguments.map(getTypeForNode);
if (sym.node.kind === SyntaxKind.ModelStatement && !sym.node.assignment) {
if (
sym.node.kind === SyntaxKind.ModelStatement ||
sym.node.kind === SyntaxKind.AliasStatement
) {
// model statement, possibly templated
if (sym.node.templateParameters.length === 0) {
if (args.length > 0) {
throwDiagnostic("Can't pass template arguments to model that is not templated", node);
program.reportDiagnostic(
"Can't pass template arguments to model that is not templated",
node
);
}
if (symbolLinks.declaredType) {
@ -235,29 +296,34 @@ export function createChecker(program: Program) {
return pendingModelType.type;
}
return checkModelStatement(sym.node);
return sym.node.kind === SyntaxKind.ModelStatement
? checkModelStatement(sym.node)
: checkAlias(sym.node);
} else {
// model is templated, lets instantiate.
// declaration is templated, lets instantiate.
if (!symbolLinks.declaredType) {
// we haven't checked the declared type yet, so do so.
checkModelStatement(sym.node);
}
if (sym.node.templateParameters!.length > node.arguments.length) {
throwDiagnostic("Too few template arguments provided.", node);
sym.node.kind === SyntaxKind.ModelStatement
? checkModelStatement(sym.node)
: checkAlias(sym.node);
}
if (sym.node.templateParameters!.length < node.arguments.length) {
throwDiagnostic("Too many template arguments provided.", node);
const templateParameters = sym.node.templateParameters;
if (args.length < templateParameters.length) {
program.reportDiagnostic("Too few template arguments provided.", node);
args = [...args, ...new Array(templateParameters.length - args.length).fill(errorType)];
} else if (args.length > templateParameters.length) {
program.reportDiagnostic("Too many template arguments provided.", node);
args = args.slice(0, templateParameters.length);
}
return instantiateTemplate(sym.node, args);
}
}
// some other kind of reference
if (args.length > 0) {
throwDiagnostic("Can't pass template arguments to non-templated type", node);
program.reportDiagnostic("Can't pass template arguments to non-templated type", node);
}
if (sym.node.kind === SyntaxKind.TemplateParameterDeclaration) {
@ -285,7 +351,10 @@ export function createChecker(program: Program) {
* twice at the same time, or if template parameters from more than one template
* are ever in scope at once.
*/
function instantiateTemplate(templateNode: ModelStatementNode, args: Type[]): ModelType {
function instantiateTemplate(
templateNode: ModelStatementNode | AliasStatementNode,
args: Type[]
): Type {
const symbolLinks = getSymbolLinks(templateNode.symbol!);
const cached = symbolLinks.instantiations!.get(args) as ModelType;
if (cached) {
@ -296,22 +365,31 @@ export function createChecker(program: Program) {
const oldTemplate = instantiatingTemplate;
templateInstantiation = args;
instantiatingTemplate = templateNode;
// this cast is invalid once we support templatized `model =`.
const type = getTypeForNode(templateNode) as ModelType;
const type = getTypeForNode(templateNode);
symbolLinks.instantiations!.set(args, type);
type.templateNode = templateNode;
if (type.kind === "Model") {
type.templateNode = templateNode;
}
templateInstantiation = oldTis;
instantiatingTemplate = oldTemplate;
return type;
}
function checkUnionExpression(node: UnionExpressionNode): UnionType {
const options = node.options.flatMap((o) => {
const type = getTypeForNode(o);
if (type.kind === "Union") {
return type.options;
}
return type;
});
return createType({
kind: "Union",
node,
options: node.options.map(getTypeForNode),
options,
});
}
@ -327,7 +405,8 @@ export function createChecker(program: Program) {
function checkIntersectionExpression(node: IntersectionExpressionNode) {
const optionTypes = node.options.map(getTypeForNode);
if (!allModelTypes(optionTypes)) {
throwDiagnostic("Cannot intersect non-model types (including union types).", node);
program.reportDiagnostic("Cannot intersect non-model types (including union types).", node);
return errorType;
}
const properties = new Map<string, ModelTypeProperty>();
@ -335,10 +414,11 @@ export function createChecker(program: Program) {
const allProps = walkPropertiesInherited(option);
for (const prop of allProps) {
if (properties.has(prop.name)) {
throwDiagnostic(
program.reportDiagnostic(
`Intersection contains duplicate property definitions for ${prop.name}`,
node
);
continue;
}
const newPropType = createType({
@ -377,19 +457,7 @@ export function createChecker(program: Program) {
}
if (Array.isArray(node.statements)) {
for (const statement of node.statements.map(getTypeForNode)) {
switch (statement.kind) {
case "Model":
type.models.set(statement.name, statement);
break;
case "Operation":
type.operations.set(statement.name, statement);
break;
case "Namespace":
type.namespaces.set(statement.name, statement);
break;
}
}
node.statements.forEach(getTypeForNode);
} else if (node.statements) {
const subNs = checkNamespace(node.statements);
type.namespaces.set(subNs.name, subNs);
@ -403,16 +471,18 @@ export function createChecker(program: Program) {
const symbolLinks = getSymbolLinks(node.symbol);
if (!symbolLinks.type) {
// haven't seen this namespace before
const namespace = getParentNamespaceType(node);
const name = node.name.sv;
const type: NamespaceType = createType({
kind: "Namespace",
name: node.name.sv,
namespace: getParentNamespaceType(node),
node: node,
name,
namespace,
node,
models: new Map(),
operations: new Map(),
namespaces: new Map(),
});
namespace?.namespaces.set(name, type);
symbolLinks.type = type;
} else {
// seen it before, need to execute the decorators on this node
@ -426,9 +496,10 @@ export function createChecker(program: Program) {
}
function getParentNamespaceType(
node: ModelStatementNode | NamespaceStatementNode | OperationStatementNode
node: ModelStatementNode | NamespaceStatementNode | OperationStatementNode | EnumStatementNode
): NamespaceType | undefined {
if (!node.namespaceSymbol) return undefined;
if (node === root.node) return undefined;
if (!node.namespaceSymbol) return root;
const symbolLinks = getSymbolLinks(node.namespaceSymbol);
compilerAssert(symbolLinks.type, "Parent namespace isn't typed yet.", node);
@ -436,14 +507,22 @@ export function createChecker(program: Program) {
}
function checkOperation(node: OperationStatementNode): OperationType {
return createType({
const namespace = getParentNamespaceType(node);
const name = node.id.sv;
const type = createType({
kind: "Operation",
name: node.id.sv,
namespace: getParentNamespaceType(node),
node: node,
name,
namespace,
node,
parameters: getTypeForNode(node.parameters) as ModelType,
returnType: getTypeForNode(node.returnType),
});
namespace?.operations.set(name, type);
return type;
}
function getGlobalNamespaceType() {
return root;
}
function checkTupleExpression(node: TupleExpressionNode): TupleType {
@ -480,6 +559,12 @@ export function createChecker(program: Program) {
}
function resolveIdentifier(node: IdentifierNode) {
if (hasParseError(node)) {
// Don't report synthetic identifiers used for parser error recovery.
// The parse error is the root cause and will already have been logged.
return undefined;
}
let scope: Node | undefined = node.parent;
let binding;
@ -509,29 +594,38 @@ export function createChecker(program: Program) {
if (binding) return binding;
}
throwDiagnostic("Unknown identifier " + node.sv, node);
program.reportDiagnostic("Unknown identifier " + node.sv, node);
return undefined;
}
function resolveTypeReference(node: ReferenceExpression): DecoratorSymbol | TypeSymbol {
function resolveTypeReference(
node: ReferenceExpression
): DecoratorSymbol | TypeSymbol | undefined {
if (node.kind === SyntaxKind.TypeReference) {
return resolveTypeReference(node.target);
}
if (node.kind === SyntaxKind.MemberExpression) {
const base = resolveTypeReference(node.base);
if (!base) {
return undefined;
}
if (base.kind === "type" && base.node.kind === SyntaxKind.NamespaceStatement) {
const symbol = resolveIdentifierInTable(node.id, base.node.exports!);
if (!symbol) {
throwDiagnostic(`Namespace doesn't have member ${node.id.sv}`, node);
program.reportDiagnostic(`Namespace doesn't have member ${node.id.sv}`, node);
return undefined;
}
return symbol;
} else if (base.kind === "decorator") {
throwDiagnostic(`Cannot resolve '${node.id.sv}' in decorator`, node);
program.reportDiagnostic(`Cannot resolve '${node.id.sv}' in decorator`, node);
return undefined;
} else {
throwDiagnostic(
program.reportDiagnostic(
`Cannot resolve '${node.id.sv}' in non-namespace node ${base.node.kind}`,
node
);
return undefined;
}
}
@ -555,12 +649,12 @@ export function createChecker(program: Program) {
}
function checkProgram(program: Program) {
reportDuplicateSymbols(program.globalNamespace.exports!);
program.reportDuplicateSymbols(program.globalNamespace.exports!);
for (const file of program.sourceFiles) {
reportDuplicateSymbols(file.locals!);
program.reportDuplicateSymbols(file.locals!);
for (const ns of file.namespaces) {
reportDuplicateSymbols(ns.locals!);
reportDuplicateSymbols(ns.exports!);
program.reportDuplicateSymbols(ns.locals!);
program.reportDuplicateSymbols(ns.exports!);
initializeTypeForNamespace(ns);
}
@ -578,11 +672,7 @@ export function createChecker(program: Program) {
function checkModel(node: ModelExpressionNode | ModelStatementNode) {
if (node.kind === SyntaxKind.ModelStatement) {
if (node.properties) {
return checkModelStatement(node);
} else {
return checkModelEquals(node);
}
return checkModelStatement(node);
} else {
return checkModelExpression(node);
}
@ -628,6 +718,7 @@ export function createChecker(program: Program) {
if (!instantiatingThisTemplate) {
links.declaredType = type;
links.instantiations = new TypeInstantiationMap();
type.namespace?.models.set(type.name, type);
}
// The model is fully created now
@ -661,7 +752,8 @@ export function createChecker(program: Program) {
for (const newProp of newProperties) {
if (properties.has(newProp.name)) {
throwDiagnostic(`Model already has a property named ${newProp.name}`, node);
program.reportDiagnostic(`Model already has a property named ${newProp.name}`, node);
continue;
}
properties.set(newProp.name, newProp);
@ -672,37 +764,21 @@ export function createChecker(program: Program) {
return properties;
}
function checkModelEquals(node: ModelStatementNode) {
// model =
// this will likely have to change, as right now `model =` is really just
// alias and so disappears. That means you can't easily rename symbols.
const assignmentType = getTypeForNode(node.assignment!);
if (assignmentType.kind === "Model") {
const type: ModelType = createType({
...assignmentType,
node: node,
name: node.id.sv,
assignmentType,
namespace: getParentNamespaceType(node),
});
return type;
}
return assignmentType;
}
function checkClassHeritage(heritage: ReferenceExpression[]): ModelType[] {
return heritage.map((heritageRef) => {
let baseModels = [];
for (let heritageRef of heritage) {
const heritageType = getTypeForNode(heritageRef);
if (heritageType.kind !== "Model") {
throwDiagnostic("Models must extend other models.", heritageRef);
if (isErrorType(heritageType)) {
compilerAssert(program.hasError(), "Should already have reported an error.", heritageRef);
continue;
}
return heritageType;
});
if (heritageType.kind !== "Model") {
program.reportDiagnostic("Models must extend other models.", heritageRef);
continue;
}
baseModels.push(heritageType);
}
return baseModels;
}
function checkSpreadProperty(targetNode: ReferenceExpression): ModelTypeProperty[] {
@ -711,7 +787,8 @@ export function createChecker(program: Program) {
if (targetType.kind != "TemplateParameter") {
if (targetType.kind !== "Model") {
throwDiagnostic("Cannot spread properties of non-model type.", targetNode);
program.reportDiagnostic("Cannot spread properties of non-model type.", targetNode);
return props;
}
// copy each property
@ -761,6 +838,57 @@ export function createChecker(program: Program) {
}
}
function checkAlias(node: AliasStatementNode): Type {
const links = getSymbolLinks(node.symbol!);
const instantiatingThisTemplate = instantiatingTemplate === node;
if (links.declaredType && !instantiatingThisTemplate) {
return links.declaredType;
}
const type = getTypeForNode(node.value);
if (!instantiatingThisTemplate) {
links.declaredType = type;
links.instantiations = new TypeInstantiationMap();
}
return type;
}
function checkEnum(node: EnumStatementNode): Type {
const links = getSymbolLinks(node.symbol!);
if (!links.type) {
const enumType: EnumType = {
kind: "Enum",
name: node.id.sv,
node,
members: [],
namespace: getParentNamespaceType(node),
};
node.members.map((m) => enumType.members.push(checkEnumMember(enumType, m)));
createType(enumType);
links.type = enumType;
}
return links.type;
}
function checkEnumMember(parentEnum: EnumType, node: EnumMemberNode): EnumMemberType {
const name = node.id.kind === SyntaxKind.Identifier ? node.id.sv : node.id.value;
const value = node.value ? node.value.value : undefined;
return createType({
kind: "EnumMember",
enum: parentEnum,
name,
node,
value,
});
}
// the types here aren't ideal and could probably be refactored.
function createType<T extends Type>(typeDef: T): T {
(typeDef as any).templateArguments = templateInstantiation;
@ -772,6 +900,7 @@ export function createChecker(program: Program) {
function getLiteralType(node: StringLiteralNode): StringLiteralType;
function getLiteralType(node: NumericLiteralNode): NumericLiteralType;
function getLiteralType(node: BooleanLiteralNode): BooleanLiteralType;
function getLiteralType(node: LiteralNode): LiteralType;
function getLiteralType(node: LiteralNode): LiteralType {
let type = program.literalTypes.get(node.value);
if (type) {
@ -794,3 +923,7 @@ export function createChecker(program: Program) {
return type;
}
}
function isErrorType(type: Type): type is ErrorType {
return type.kind === "Intrinsic" && type.name === "ErrorType";
}

Просмотреть файл

@ -1,4 +1,4 @@
import { spawnSync } from "child_process";
import { spawnSync, SpawnSyncOptions } from "child_process";
import { mkdtemp, readdir, rmdir } from "fs/promises";
import mkdirp from "mkdirp";
import os from "os";
@ -7,17 +7,19 @@ import url from "url";
import yargs from "yargs";
import { CompilerOptions } from "../compiler/options.js";
import { compile } from "../compiler/program.js";
import { compilerAssert, DiagnosticError, dumpError, logDiagnostics } from "./diagnostics.js";
import { loadADLConfigInDir } from "../config/index.js";
import { compilerAssert, dumpError, logDiagnostics } from "./diagnostics.js";
import { formatADLFiles } from "./formatter.js";
import { adlVersion, NodeHost } from "./util.js";
const args = yargs(process.argv.slice(2))
.scriptName("adl")
.help()
.strict()
.command("compile <path>", "Compile a directory of ADL files.", (cmd) => {
.command("compile <path>", "Compile ADL source.", (cmd) => {
return cmd
.positional("path", {
description: "The path to folder containing .adl files",
description: "The path to the main.adl file or directory containing main.adl.",
type: "string",
})
.option("output-path", {
@ -38,11 +40,9 @@ const args = yargs(process.argv.slice(2))
describe: "Don't load the ADL standard library.",
});
})
.command(
"generate <path>",
"Generate client and server code from a directory of ADL files.",
(cmd) => {
return cmd
.command("generate <path>", "Generate client code from ADL source.", (cmd) => {
return (
cmd
.positional("path", {
description: "The path to folder containing .adl files",
type: "string",
@ -67,9 +67,13 @@ const args = yargs(process.argv.slice(2))
string: true,
describe:
"Key/value pairs that can be passed to ADL components. The format is 'key=value'. This parameter can be used multiple times to add more options.",
});
}
)
})
// we can't generate anything but a client yet
.demandOption("client")
// and language is required to do so
.demandOption("language")
);
})
.command("code", "Manage VS Code Extension.", (cmd) => {
return cmd
.demandCommand(1, "No command specified.")
@ -83,6 +87,14 @@ const args = yargs(process.argv.slice(2))
.command("install", "Install Visual Studio Extension.")
.command("uninstall", "Uninstall VS Extension");
})
.command("format <include...>", "Format given list of adl files.", (cmd) => {
return cmd.positional("include", {
description: "Wildcard pattern of the list of files.",
type: "string",
array: true,
});
})
.command("info", "Show information about current ADL compiler.")
.option("debug", {
type: "boolean",
description: "Output debug log messages.",
@ -90,18 +102,16 @@ const args = yargs(process.argv.slice(2))
.version(adlVersion)
.demandCommand(1, "You must use one of the supported commands.").argv;
async function compileInput(compilerOptions: CompilerOptions) {
try {
await compile(args.path!, NodeHost, compilerOptions);
} catch (err) {
if (err instanceof DiagnosticError) {
logDiagnostics(err.diagnostics, console.error);
if (args.debug) {
console.error(`Stack trace:\n\n${err.stack}`);
}
process.exit(1);
}
throw err; // let non-diagnostic errors go to top-level bug handler.
async function compileInput(compilerOptions: CompilerOptions, printSuccess = true) {
const program = await compile(args.path!, NodeHost, compilerOptions);
logDiagnostics(program.diagnostics, console.error);
if (program.hasError()) {
process.exit(1);
}
if (printSuccess) {
console.log(
`Compilation completed successfully, output files are in ${compilerOptions.outputPath}.`
);
}
}
@ -123,6 +133,7 @@ async function getCompilerOptions(): Promise<CompilerOptions> {
return {
miscOptions,
outputPath,
swaggerOutputFile: resolve(args["output-path"], "openapi.json"),
nostdlib: args["nostdlib"],
};
@ -134,7 +145,8 @@ async function generateClient(options: CompilerOptions) {
const autoRestPath = new url.URL(`../../node_modules/.bin/${autoRestBin}`, import.meta.url);
// Execute AutoRest on the output file
const result = spawnSync(
console.log(); //newline between compilation output and generation output
const result = run(
url.fileURLToPath(autoRestPath),
[
`--${args.language}`,
@ -144,15 +156,14 @@ async function generateClient(options: CompilerOptions) {
`--input-file=${options.swaggerOutputFile}`,
],
{
stdio: "inherit",
shell: true,
}
);
if (result.status === 0) {
console.log(`Generation completed successfully, output files are in ${options.outputPath}.`);
console.log(`\nGeneration completed successfully, output files are in ${clientPath}.`);
} else {
console.error("\nAn error occurred during client generation.");
console.error("\nClient generation failed.");
process.exit(result.status || 1);
}
}
@ -160,7 +171,20 @@ async function generateClient(options: CompilerOptions) {
async function installVsix(pkg: string, install: (vsixPath: string) => void) {
// download npm package to temporary directory
const temp = await mkdtemp(path.join(os.tmpdir(), "adl"));
run("npm", ["install", "--silent", "--prefix", temp, pkg]);
const npmArgs = ["install"];
// hide npm output unless --debug was passed to adl
if (!args.debug) {
npmArgs.push("--silent");
}
// NOTE: Using cwd=temp with `--prefix .` instead of `--prefix ${temp}` to
// workaround https://github.com/npm/cli/issues/3256. It's still important
// to pass --prefix even though we're using cwd as otherwise, npm might
// find a package.json file in a parent directory and install to that
// directory.
npmArgs.push("--prefix", ".", pkg);
run("npm", npmArgs, { cwd: temp });
// locate .vsix
const dir = path.join(temp, "node_modules", pkg);
@ -182,14 +206,21 @@ async function installVsix(pkg: string, install: (vsixPath: string) => void) {
await rmdir(temp, { recursive: true });
}
async function runCode(codeArgs: string[]) {
await run(args.insiders ? "code-insiders" : "code", codeArgs, {
// VS Code's CLI emits node warnings that we can't do anything about. Suppress them.
extraEnv: { NODE_NO_WARNINGS: "1" },
});
}
async function installVSCodeExtension() {
await installVsix("adl-vscode", (vsix) => {
run(args.insiders ? "code-insiders" : "code", ["--install-extension", vsix]);
runCode(["--install-extension", vsix]);
});
}
async function uninstallVSCodeExtension() {
run(args.insiders ? "code-insiders" : "code", ["--uninstall-extension", "microsoft.adl-vscode"]);
await runCode(["--uninstall-extension", "microsoft.adl-vscode"]);
}
function getVsixInstallerPath(): string {
@ -217,13 +248,49 @@ async function uninstallVSExtension() {
run(vsixInstaller, ["/uninstall:88b9492f-c019-492c-8aeb-f325a7e4cf23"]);
}
/**
* Print the resolved adl configuration.
*/
async function printInfo() {
const cwd = process.cwd();
console.log(`Module: ${url.fileURLToPath(import.meta.url)}`);
const config = await loadADLConfigInDir(cwd);
const jsyaml = await import("js-yaml");
const excluded = ["diagnostics", "filename"];
const replacer = (key: string, value: any) => (excluded.includes(key) ? undefined : value);
console.log(`User Config: ${config.filename ?? "No config file found"}`);
console.log("-----------");
console.log(jsyaml.dump(config, { replacer }));
console.log("-----------");
logDiagnostics(config.diagnostics, console.error);
if (config.diagnostics.some((d) => d.severity === "error")) {
process.exit(1);
}
}
// NOTE: We could also use { shell: true } to let windows find the .cmd, but that breaks
// ENOENT checking and handles spaces poorly in some cases.
const isCmdOnWindows = ["code", "code-insiders", "npm"];
function run(command: string, commandArgs: string[]) {
interface RunOptions extends SpawnSyncOptions {
extraEnv?: NodeJS.ProcessEnv;
}
function run(command: string, commandArgs: string[], options?: RunOptions) {
if (args.debug) {
console.log(`> ${command} ${commandArgs.join(" ")}`);
if (options) {
console.log(options);
}
console.log(`> ${command} ${commandArgs.join(" ")}\n`);
}
if (options?.extraEnv) {
options.env = {
...(options?.env ?? process.env),
...options.extraEnv,
};
}
const baseCommandName = path.basename(command);
@ -233,8 +300,7 @@ function run(command: string, commandArgs: string[]) {
const proc = spawnSync(command, commandArgs, {
stdio: "inherit",
// VS Code's CLI emits node warnings that we can't do anything about. Suppress them.
env: { ...process.env, NODE_NO_WARNINGS: "1" },
...(options ?? {}),
});
if (proc.error) {
@ -255,8 +321,10 @@ function run(command: string, commandArgs: string[]) {
proc.status
}.`
);
process.exit(proc.status ?? 1);
process.exit(proc.status || 1);
}
return proc;
}
async function main() {
@ -266,13 +334,16 @@ async function main() {
let action: string | number;
switch (command) {
case "info":
printInfo();
break;
case "compile":
options = await getCompilerOptions();
await compileInput(options);
break;
case "generate":
options = await getCompilerOptions();
await compileInput(options);
await compileInput(options, false);
if (args.client) {
await generateClient(options);
}
@ -298,18 +369,27 @@ async function main() {
await uninstallVSExtension();
break;
}
break;
case "format":
await formatADLFiles(args["include"]!, { debug: args.debug });
break;
}
}
main()
.then(() => {})
.catch((err) => {
// NOTE: An expected error, like one thrown for bad input, shouldn't reach
// here, but be handled somewhere else. If we reach here, it should be
// considered a bug and therefore we should not suppress the stack trace as
// that risks losing it in the case of a bug that does not repro easily.
console.error("Internal compiler error!");
console.error("File issue at https://github.com/azure/adl");
dumpError(err, console.error);
process.exit(1);
});
function internalCompilerError(error: Error) {
// NOTE: An expected error, like one thrown for bad input, shouldn't reach
// here, but be handled somewhere else. If we reach here, it should be
// considered a bug and therefore we should not suppress the stack trace as
// that risks losing it in the case of a bug that does not repro easily.
console.error("Internal compiler error!");
console.error("File issue at https://github.com/azure/adl");
dumpError(error, console.error);
process.exit(1);
}
process.on("unhandledRejection", (error: Error) => {
console.error("Unhandled promise rejection!");
internalCompilerError(error);
});
main().catch(internalCompilerError);

Просмотреть файл

@ -1,23 +1,10 @@
import { AssertionError } from "assert";
import { CharacterCodes } from "./character-codes.js";
import { CharCode, isNonAsciiLineBreak } from "./charcode.js";
import { Message } from "./messages.js";
import { Diagnostic, Node, SourceFile, SourceLocation, Sym, SyntaxKind, Type } from "./types.js";
export { Message } from "./messages.js";
/**
* Represents an error in the code input that is fatal and bails the compilation.
*
* This isn't meant to be kept long term, but we currently do this on all errors.
*/
export class DiagnosticError extends Error {
constructor(public readonly diagnostics: readonly Diagnostic[]) {
super("Code diagnostics. See diagnostics array.");
// Tests don't have our catch-all handler so log the diagnostic now.
logVerboseTestOutput((log) => logDiagnostics(diagnostics, log));
}
}
/**
* Represents a failure with multiple errors.
*/
@ -32,38 +19,25 @@ export class AggregateError extends Error {
}
}
export const NoTarget = Symbol("NoTarget");
export type DiagnosticTarget = Node | Type | Sym | SourceLocation;
export type WriteLine = (text?: string) => void;
export type ErrorHandler = (
message: Message | string,
target: DiagnosticTarget,
args?: (string | number)[]
) => void;
export const throwOnError: ErrorHandler = throwDiagnostic;
export function throwDiagnostic(
message: Message | string,
target: DiagnosticTarget,
args?: (string | number)[]
): never {
throw new DiagnosticError([createDiagnostic(message, target, args)]);
}
export type DiagnosticHandler = (diagnostic: Diagnostic) => void;
export function createDiagnostic(
message: Message | string,
target: DiagnosticTarget,
target: DiagnosticTarget | typeof NoTarget,
args?: (string | number)[]
): Diagnostic {
let location: SourceLocation;
let location: Partial<SourceLocation> = {};
let locationError: Error | undefined;
try {
location = getSourceLocation(target);
} catch (err) {
locationError = err;
location = createDummySourceLocation();
if (target !== NoTarget) {
try {
location = getSourceLocation(target);
} catch (err) {
locationError = err;
location = createDummySourceLocation();
}
}
if (typeof message === "string") {
@ -80,7 +54,10 @@ export function createDiagnostic(
};
if (locationError || formatError) {
throw new AggregateError(new DiagnosticError([diagnostic]), locationError, formatError);
const diagnosticError = new Error(
"Error(s) occurred trying to report diagnostic: " + diagnostic.message
);
throw new AggregateError(diagnosticError, locationError, formatError);
}
return diagnostic;
@ -94,12 +71,17 @@ export function logDiagnostics(diagnostics: readonly Diagnostic[], writeLine: Wr
export function formatDiagnostic(diagnostic: Diagnostic) {
const code = diagnostic.code ? ` ADL${diagnostic.code}` : "";
const pos = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.pos);
const line = pos.line + 1;
const col = pos.character + 1;
const severity = diagnostic.severity;
const path = diagnostic.file.path;
return `${path}:${line}:${col} - ${severity}${code}: ${diagnostic.message}`;
const content = `${severity}${code}: ${diagnostic.message}`;
if (diagnostic.file) {
const pos = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.pos ?? 0);
const line = pos.line + 1;
const col = pos.character + 1;
const path = diagnostic.file.path;
return `${path}:${line}:${col} - ${content}`;
} else {
return content;
}
}
export function createSourceFile(text: string, path: string): SourceFile {
@ -113,7 +95,7 @@ export function createSourceFile(text: string, path: string): SourceFile {
};
function getLineStarts() {
return (lineStarts = lineStarts ?? scanLineStarts());
return (lineStarts = lineStarts ?? scanLineStarts(text));
}
function getLineAndCharacterOfPosition(position: number) {
@ -136,57 +118,6 @@ export function createSourceFile(text: string, path: string): SourceFile {
character: position - starts[line],
};
}
function scanLineStarts() {
const starts = [];
let start = 0;
let pos = 0;
while (pos < text.length) {
const ch = text.charCodeAt(pos);
pos++;
switch (ch) {
case CharacterCodes.carriageReturn:
if (text.charCodeAt(pos) === CharacterCodes.lineFeed) {
pos++;
}
// fallthrough
case CharacterCodes.lineFeed:
case CharacterCodes.lineSeparator:
case CharacterCodes.paragraphSeparator:
starts.push(start);
start = pos;
break;
}
}
starts.push(start);
return starts;
}
/**
* Search sorted array of numbers for the given value. If found, return index
* in array where value was found. If not found, return a negative number that
* is the bitwise complement of the index where value would need to be inserted
* to keep the array sorted.
*/
function binarySearch(array: readonly number[], value: number) {
let low = 0;
let high = array.length - 1;
while (low <= high) {
const middle = low + ((high - low) >> 1);
const v = array[middle];
if (v < value) {
low = middle + 1;
} else if (v > value) {
high = middle - 1;
} else {
return middle;
}
}
return ~low;
}
}
export function getSourceLocation(target: DiagnosticTarget): SourceLocation {
@ -250,10 +181,7 @@ export function logVerboseTestOutput(messageOrCallback: string | ((log: WriteLin
}
export function dumpError(error: Error, writeLine: WriteLine) {
if (error instanceof DiagnosticError) {
logDiagnostics(error.diagnostics, writeLine);
writeLine(error.stack);
} else if (error instanceof AggregateError) {
if (error instanceof AggregateError) {
for (const inner of error.errors) {
dumpError(inner, writeLine);
}
@ -328,3 +256,58 @@ function format(text: string, args?: (string | number)[]): [string, Error?] {
function isNotUndefined<T>(value: T | undefined): value is T {
return value !== undefined;
}
function scanLineStarts(text: string): number[] {
const starts = [];
let start = 0;
let pos = 0;
while (pos < text.length) {
const ch = text.charCodeAt(pos);
pos++;
switch (ch) {
case CharCode.CarriageReturn:
if (text.charCodeAt(pos) === CharCode.LineFeed) {
pos++;
}
// fallthrough
case CharCode.LineFeed:
starts.push(start);
start = pos;
break;
default:
if (ch > CharCode.MaxAscii && isNonAsciiLineBreak(ch)) {
starts.push(start);
start = pos;
break;
}
}
}
starts.push(start);
return starts;
}
/**
* Search sorted array of numbers for the given value. If found, return index
* in array where value was found. If not found, return a negative number that
* is the bitwise complement of the index where value would need to be inserted
* to keep the array sorted.
*/
function binarySearch(array: readonly number[], value: number) {
let low = 0;
let high = array.length - 1;
while (low <= high) {
const middle = low + ((high - low) >> 1);
const v = array[middle];
if (v < value) {
low = middle + 1;
} else if (v > value) {
high = middle - 1;
} else {
return middle;
}
}
return ~low;
}

Просмотреть файл

@ -0,0 +1,55 @@
import { readFile, writeFile } from "fs/promises";
import glob from "glob";
import prettier from "prettier";
import * as adlPrettierPlugin from "../formatter/index.js";
import { PrettierParserError } from "../formatter/parser.js";
export async function formatADL(code: string): Promise<string> {
const output = prettier.format(code, {
parser: "adl",
plugins: [adlPrettierPlugin],
});
return output;
}
/**
* Format all the adl files.
* @param patterns List of wildcard pattern searching for adl files.
*/
export async function formatADLFiles(patterns: string[], { debug }: { debug?: boolean }) {
const files = await findFiles(patterns);
for (const file of files) {
try {
await formatADLFile(file);
} catch (e) {
if (e instanceof PrettierParserError) {
const details = debug ? e.message : "";
console.error(`File '${file}' failed to fromat. ${details}`);
} else {
throw e;
}
}
}
}
export async function formatADLFile(filename: string) {
const content = await readFile(filename);
const formattedContent = await formatADL(content.toString());
await writeFile(filename, formattedContent);
}
async function findFilesFromPattern(pattern: string): Promise<string[]> {
return new Promise((resolve, reject) => {
glob(pattern, (err, matches) => {
if (err) {
reject(err);
}
resolve(matches);
});
});
}
async function findFiles(include: string[]): Promise<string[]> {
const result = await Promise.all(include.map((path) => findFilesFromPattern(path)));
return result.flat();
}

Просмотреть файл

@ -1,4 +1,8 @@
export * from "../lib/decorators.js";
export * from "./diagnostics.js";
export * from "./mutators.js";
export * from "./parser.js";
export * from "./program.js";
export * from "./types.js";
import * as formatter from "../formatter/index.js";
export const ADLPrettierPlugin = formatter;

Просмотреть файл

@ -8,50 +8,50 @@ export const Message = {
DigitExpected: {
code: 1100,
severity: "error",
text: "Digit expected (0-9)",
text: "Digit expected.",
} as const,
HexDigitExpected: {
code: 1101,
severity: "error",
text: "Hex Digit expected (0-F)",
text: "Hexadecimal digit expected.",
} as const,
BinaryDigitExpected: {
code: 1102,
severity: "error",
text: "Binary Digit expected (0,1)",
text: "Binary digit expected.",
} as const,
UnexpectedEndOfFile: {
Unterminated: {
code: 1103,
severity: "error",
text: "Unexpected end of file while searching for '{0}'",
text: "Unterminated {0}.",
} as const,
InvalidEscapeSequence: {
code: 1104,
severity: "error",
text: "Invalid escape sequence",
text: "Invalid escape sequence.",
} as const,
NoNewLineAtStartOfTripleQuotedString: {
code: 1105,
severity: "error",
text: "String content in triple quotes must begin on a new line",
text: "String content in triple quotes must begin on a new line.",
} as const,
NoNewLineAtEndOfTripleQuotedString: {
code: 1106,
severity: "error",
text: "Closing triple quotes must begin on a new line",
text: "Closing triple quotes must begin on a new line.",
} as const,
InconsistentTripleQuoteIndentation: {
code: 1107,
severity: "error",
text:
"All lines in triple-quoted string lines must have the same indentation as closing triple quotes",
"All lines in triple-quoted string lines must have the same indentation as closing triple quotes.",
} as const,
InvalidCharacter: {
@ -59,6 +59,12 @@ export const Message = {
severity: "error",
text: "Invalid character.",
} as const,
FileNotFound: {
code: 1109,
text: `File {0} not found.`,
severity: "error",
} as const,
};
// Static assert: this won't compile if one of the entries above is invalid.

Просмотреть файл

@ -0,0 +1,186 @@
import { createBinder } from "./binder.js";
import { parse } from "./parser.js";
import { Program } from "./program.js";
import {
ModelExpressionNode,
ModelPropertyNode,
ModelStatementNode,
ModelType,
ModelTypeProperty,
Node,
OperationStatementNode,
OperationType,
SyntaxKind,
UnionExpressionNode,
} from "./types.js";
function addProperty(
program: Program,
model: ModelType,
modelNode: ModelStatementNode | ModelExpressionNode,
parentNode: Node,
propertyName: string,
propertyTypeName: string,
insertIndex?: number
): ModelTypeProperty | undefined {
// Parse a temporary model type to extract its property
const fakeNode = parse(`model Fake { ${propertyName}: ${propertyTypeName}}`);
if (fakeNode.parseDiagnostics.length > 0) {
program.reportDiagnostic(
`Could not add property/parameter "${propertyName}" of type "${propertyTypeName}"`,
model
);
program.reportDiagnostics(fakeNode.parseDiagnostics);
return undefined;
}
const firstStatement = fakeNode.statements[0] as ModelStatementNode;
const graftProperty = firstStatement.properties![0] as ModelPropertyNode;
// Fix up the source location of the nodes to match the model node that
// contains the new property since we can't update the entire file's node
// positions.
graftProperty.pos = modelNode.pos;
graftProperty.end = modelNode.end;
// Create a binder to wire up the grafted property
const binder = createBinder(program, {
initialParentNode: parentNode,
});
binder.bindNode(graftProperty);
// Evaluate the new property with the checker
const newProperty = program.checker!.checkModelProperty(graftProperty);
// Put the property back into the node
modelNode.properties.splice(insertIndex || modelNode.properties.length, 0, graftProperty);
if (insertIndex !== undefined) {
// Insert the property by adding it in the right order to a new Map
let i = 0;
const newProperties = new Map<string, ModelTypeProperty>();
for (let [name, prop] of model.properties.entries()) {
if (i === insertIndex) {
newProperties.set(newProperty.name, newProperty);
}
newProperties.set(name, prop);
model.properties = newProperties;
i++;
}
} else {
model.properties.set(newProperty.name, newProperty);
}
return newProperty;
}
export function addModelProperty(
program: Program,
model: ModelType,
propertyName: string,
propertyTypeName: string
): ModelTypeProperty | undefined {
if (model.node.kind !== SyntaxKind.ModelStatement) {
program.reportDiagnostic(
"Cannot add a model property to anything except a model statement.",
model
);
return;
}
// Create the property and add it to the type
const newProperty = addProperty(
program,
model,
model.node,
model.node,
propertyName,
propertyTypeName
);
if (newProperty) {
model.properties.set(propertyName, newProperty);
return newProperty;
}
return undefined;
}
export interface NewParameterOptions {
// Insert the parameter at the specified index. If `undefined`, add the
// parameter to the end of the parameter list.
insertIndex?: number;
}
export function addOperationParameter(
program: Program,
operation: OperationType,
parameterName: string,
parameterTypeName: string,
options?: NewParameterOptions
): ModelTypeProperty | undefined {
if (operation.node.kind !== SyntaxKind.OperationStatement) {
program.reportDiagnostic(
"Cannot add a parameter to anything except an operation statement.",
operation
);
return;
}
// Create the property and add it to the type
return addProperty(
program,
operation.parameters,
operation.node.parameters,
operation.node,
parameterName,
parameterTypeName,
options?.insertIndex
);
}
export function addOperationResponseType(
program: Program,
operation: OperationType,
responseTypeName: string
): any {
if (operation.node.kind !== SyntaxKind.OperationStatement) {
program.reportDiagnostic(
"Cannot add a response to anything except an operation statement.",
operation
);
return;
}
// Parse a temporary operation to extract its response type
const opNode = parse(`op Fake(): string | ${responseTypeName};`);
if (opNode.parseDiagnostics.length > 0) {
program.reportDiagnostic(
`Could not add response type "${responseTypeName}" to operation ${operation.name}"`,
operation
);
program.reportDiagnostics(opNode.parseDiagnostics);
return undefined;
}
const graftUnion = (opNode.statements[0] as OperationStatementNode)
.returnType as UnionExpressionNode;
// Graft the union into the operation
const originalResponse = operation.node.returnType;
graftUnion.options[0] = originalResponse;
operation.node.returnType = graftUnion;
// Create a binder to wire up the grafted property
const binder = createBinder(program, {
initialParentNode: operation.node,
});
binder.bindNode(graftUnion);
// Evaluate the new response type with the checker
operation.returnType = program.checker!.checkUnionExpression(graftUnion);
}

Просмотреть файл

@ -4,15 +4,19 @@
//
// Based on:
// - http://www.unicode.org/reports/tr31/
// - https://www.ecma-international.org/ecma-262/6.0/#sec-names-and-keywords
// - https://www.ecma-international.org/ecma-262/11.0/#sec-names-and-keywords
//
// ADL's identifier naming rules are currently the same as JavaScript's.
//
/**
* @internal
*
* Map of non-ascii characters that are valid at the start of an identifier.
* Each pair of numbers represents an inclusive range of code points.
*
* Corresponds to code points outside the ASCII range with property ID_Start or
* Other_ID_Start.
*/
// prettier-ignore
export const nonAsciiIdentifierStartMap: readonly number[] = [
@ -641,8 +645,12 @@ export const nonAsciiIdentifierStartMap: readonly number[] = [
/**
* @internal
*
* Map of non-ascii chacters that are valid after the first character in and identifier.
* Each pair of numbers represents an inclusive range of code points.
* Map of non-ascii chacters that are valid after the first character in and
* identifier. Each pair of numbers represents an inclusive range of code
* points.
*
* Corresponds to code points outside the ASCII range with property ID_Continue,
* Other_ID_Start, or Other_ID_Continue, plus ZWNJ and ZWJ.
*/
//prettier-ignore
export const nonAsciiIdentifierContinueMap: readonly number[] = [
@ -943,6 +951,7 @@ export const nonAsciiIdentifierContinueMap: readonly number[] = [
0x1fe0, 0x1fec,
0x1ff2, 0x1ff4,
0x1ff6, 0x1ffc,
0x200c, 0x200d,
0x203f, 0x2040,
0x2054, 0x2054,
0x2071, 0x2071,

Просмотреть файл

@ -1,6 +1,5 @@
export interface CompilerOptions {
miscOptions?: any;
mainFile?: string;
outputPath?: string;
swaggerOutputFile?: string;
nostdlib?: boolean;

Просмотреть файл

@ -1,7 +1,8 @@
import { createSymbolTable } from "./binder.js";
import { compilerAssert, createDiagnostic, DiagnosticTarget, Message } from "./diagnostics.js";
import { compilerAssert, createDiagnostic } from "./diagnostics.js";
import {
createScanner,
isComment,
isKeyword,
isPunctuation,
isStatementKeyword,
@ -11,10 +12,14 @@ import {
} from "./scanner.js";
import {
ADLScriptNode,
AliasStatementNode,
BooleanLiteralNode,
Comment,
DecoratorExpressionNode,
Diagnostic,
EmptyStatementNode,
EnumMemberNode,
EnumStatementNode,
Expression,
IdentifierNode,
ImportStatementNode,
@ -127,6 +132,10 @@ namespace ListKind {
toleratedDelimiter: Token.Comma,
} as const;
export const EnumMembers = {
...ModelProperties,
} as const;
const ExpresionsBase = {
allowEmpty: true,
delimiter: Token.Comma,
@ -162,13 +171,21 @@ namespace ListKind {
} as const;
}
export function parse(code: string | SourceFile) {
export interface ParseOptions {
/**
* Include comments in resulting output.
*/
comments?: boolean;
}
export function parse(code: string | SourceFile, options: ParseOptions = {}): ADLScriptNode {
let parseErrorInNextFinishedNode = false;
let previousTokenEnd = -1;
let realPositionOfLastError = -1;
let missingIdentifierCounter = 0;
const parseDiagnostics: Diagnostic[] = [];
const scanner = createScanner(code, reportDiagnostic);
const comments: Comment[] = [];
nextToken();
return parseADLScript();
@ -186,7 +203,8 @@ export function parse(code: string | SourceFile) {
locals: createSymbolTable(),
inScopeNamespaces: [],
parseDiagnostics,
...finishNode({}, 0),
comments,
...finishNode(0),
};
}
@ -212,6 +230,13 @@ export function parse(code: string | SourceFile) {
case Token.OpKeyword:
item = parseOperationStatement(decorators);
break;
case Token.EnumKeyword:
item = parseEnumStatement(decorators);
break;
case Token.AliasKeyword:
reportInvalidDecorators(decorators, "alias statement");
item = parseAliasStatement();
break;
case Token.UsingKeyword:
reportInvalidDecorators(decorators, "using statement");
item = parseUsingStatement();
@ -275,12 +300,19 @@ export function parse(code: string | SourceFile) {
case Token.OpKeyword:
stmts.push(parseOperationStatement(decorators));
break;
case Token.EnumKeyword:
stmts.push(parseEnumStatement(decorators));
break;
case Token.AliasKeyword:
reportInvalidDecorators(decorators, "alias statement");
stmts.push(parseAliasStatement());
break;
case Token.UsingKeyword:
reportInvalidDecorators(decorators, "using statement");
stmts.push(parseUsingStatement());
break;
case Token.EndOfFile:
error("End of file reached without '}'.");
parseExpected(Token.CloseBrace);
return stmts;
case Token.Semicolon:
reportInvalidDecorators(decorators, "empty statement");
@ -316,8 +348,6 @@ export function parse(code: string | SourceFile) {
}
nsSegments.push(currentName);
let parameters: ModelExpressionNode | undefined;
const nextTok = parseExpectedOneOf(Token.Semicolon, Token.OpenBrace);
let statements: Statement[] | undefined;
@ -326,28 +356,22 @@ export function parse(code: string | SourceFile) {
parseExpected(Token.CloseBrace);
}
let outerNs: NamespaceStatementNode = finishNode(
{
kind: SyntaxKind.NamespaceStatement,
decorators,
name: nsSegments[0],
parameters,
statements,
},
nsSegments[0].pos
);
let outerNs: NamespaceStatementNode = {
kind: SyntaxKind.NamespaceStatement,
decorators,
name: nsSegments[0],
statements,
...finishNode(nsSegments[0].pos),
};
for (let i = 1; i < nsSegments.length; i++) {
outerNs = finishNode(
{
kind: SyntaxKind.NamespaceStatement,
decorators: [],
name: nsSegments[i],
parameters,
statements: outerNs,
},
nsSegments[i].pos
);
outerNs = {
kind: SyntaxKind.NamespaceStatement,
decorators: [],
name: nsSegments[i],
statements: outerNs,
...finishNode(nsSegments[i].pos),
};
}
return outerNs;
@ -359,13 +383,11 @@ export function parse(code: string | SourceFile) {
const name = parseIdentifierOrMemberExpression();
parseExpected(Token.Semicolon);
return finishNode(
{
kind: SyntaxKind.UsingStatement,
name,
},
pos
);
return {
kind: SyntaxKind.UsingStatement,
name,
...finishNode(pos),
};
}
function parseOperationStatement(decorators: DecoratorExpressionNode[]): OperationStatementNode {
@ -379,28 +401,24 @@ export function parse(code: string | SourceFile) {
const returnType = parseExpression();
parseExpected(Token.Semicolon);
return finishNode(
{
kind: SyntaxKind.OperationStatement,
id,
parameters,
returnType,
decorators,
},
pos
);
return {
kind: SyntaxKind.OperationStatement,
id,
parameters,
returnType,
decorators,
...finishNode(pos),
};
}
function parseOperationParameters(): ModelExpressionNode {
const pos = tokenPos();
const properties = parseList(ListKind.OperationParameters, parseModelPropertyOrSpread);
const parameters: ModelExpressionNode = finishNode(
{
kind: SyntaxKind.ModelExpression,
properties,
},
pos
);
const parameters: ModelExpressionNode = {
kind: SyntaxKind.ModelExpression,
properties,
...finishNode(pos),
};
return parameters;
}
@ -416,37 +434,18 @@ export function parse(code: string | SourceFile) {
expectTokenIsOneOf(Token.OpenBrace, Token.Equals, Token.ExtendsKeyword);
if (parseOptional(Token.Equals)) {
const assignment = parseExpression();
parseExpected(Token.Semicolon);
const heritage: ReferenceExpression[] = parseOptionalModelHeritage();
const properties = parseList(ListKind.ModelProperties, parseModelPropertyOrSpread);
return finishNode(
{
kind: SyntaxKind.ModelStatement,
id,
heritage: [],
templateParameters,
assignment,
decorators,
},
pos
);
} else {
const heritage: ReferenceExpression[] = parseOptionalModelHeritage();
const properties = parseList(ListKind.ModelProperties, parseModelPropertyOrSpread);
return finishNode(
{
kind: SyntaxKind.ModelStatement,
id,
heritage,
templateParameters,
decorators,
properties,
},
pos
);
}
return {
kind: SyntaxKind.ModelStatement,
id,
heritage,
templateParameters,
decorators,
properties,
...finishNode(pos),
};
}
function parseOptionalModelHeritage() {
@ -463,13 +462,11 @@ export function parse(code: string | SourceFile) {
): TemplateParameterDeclarationNode {
reportInvalidDecorators(decorators, "template parameter");
const id = parseIdentifier();
return finishNode(
{
kind: SyntaxKind.TemplateParameterDeclaration,
id,
},
pos
);
return {
kind: SyntaxKind.TemplateParameterDeclaration,
id,
...finishNode(pos),
};
}
function parseModelPropertyOrSpread(pos: number, decorators: DecoratorExpressionNode[]) {
@ -489,20 +486,18 @@ export function parse(code: string | SourceFile) {
// This could be broadened to allow any type expression
const target = parseReferenceExpression();
return finishNode(
{
kind: SyntaxKind.ModelSpreadProperty,
target,
},
pos
);
return {
kind: SyntaxKind.ModelSpreadProperty,
target,
...finishNode(pos),
};
}
function parseModelProperty(
pos: number,
decorators: DecoratorExpressionNode[]
): ModelPropertyNode | ModelSpreadPropertyNode {
let id =
const id =
token() === Token.StringLiteral
? parseStringLiteral()
: parseIdentifier("Property expected.");
@ -511,16 +506,76 @@ export function parse(code: string | SourceFile) {
parseExpected(Token.Colon);
const value = parseExpression();
return finishNode(
{
kind: SyntaxKind.ModelProperty,
id,
decorators,
value,
optional,
},
pos
return {
kind: SyntaxKind.ModelProperty,
id,
decorators,
value,
optional,
...finishNode(pos),
};
}
function parseEnumStatement(decorators: DecoratorExpressionNode[]): EnumStatementNode {
const pos = tokenPos();
parseExpected(Token.EnumKeyword);
const id = parseIdentifier();
const members = parseList(ListKind.EnumMembers, parseEnumMember);
return {
kind: SyntaxKind.EnumStatement,
id,
decorators,
members,
...finishNode(pos),
};
}
function parseEnumMember(pos: number, decorators: DecoratorExpressionNode[]): EnumMemberNode {
const id =
token() === Token.StringLiteral
? parseStringLiteral()
: parseIdentifier("Enum member expected.");
let value: StringLiteralNode | NumericLiteralNode | undefined;
if (parseOptional(Token.Colon)) {
const expr = parseExpression();
if (expr.kind === SyntaxKind.StringLiteral || expr.kind === SyntaxKind.NumericLiteral) {
value = expr;
} else if (getFlag(expr, NodeFlags.ThisNodeHasError)) {
parseErrorInNextFinishedNode = true;
} else {
error("Expected numeric or string literal", expr);
}
}
return {
kind: SyntaxKind.EnumMember,
id,
value,
decorators,
...finishNode(pos),
};
}
function parseAliasStatement(): AliasStatementNode {
const pos = tokenPos();
parseExpected(Token.AliasKeyword);
const id = parseIdentifier();
const templateParameters = parseOptionalList(
ListKind.TemplateParameters,
parseTemplateParameter
);
parseExpected(Token.Equals);
const value = parseExpression();
parseExpected(Token.Semicolon);
return {
kind: SyntaxKind.AliasStatement,
id,
templateParameters,
value,
...finishNode(pos),
};
}
function parseExpression(): Expression {
@ -536,22 +591,17 @@ export function parse(code: string | SourceFile) {
return node;
}
node = finishNode(
{
kind: SyntaxKind.UnionExpression,
options: [node],
},
pos
);
const options = [node];
while (parseOptional(Token.Bar)) {
const expr = parseIntersectionExpressionOrHigher();
node.options.push(expr);
options.push(expr);
}
node.end = tokenPos();
return node;
return {
kind: SyntaxKind.UnionExpression,
options,
...finishNode(pos),
};
}
function parseIntersectionExpressionOrHigher(): Expression {
@ -563,22 +613,17 @@ export function parse(code: string | SourceFile) {
return node;
}
node = finishNode(
{
kind: SyntaxKind.IntersectionExpression,
options: [node],
},
pos
);
const options = [node];
while (parseOptional(Token.Ampersand)) {
const expr = parseArrayExpressionOrHigher();
node.options.push(expr);
options.push(expr);
}
node.end = tokenPos();
return node;
return {
kind: SyntaxKind.IntersectionExpression,
options,
...finishNode(pos),
};
}
function parseArrayExpressionOrHigher(): Expression {
@ -588,13 +633,11 @@ export function parse(code: string | SourceFile) {
while (parseOptional(Token.OpenBracket)) {
parseExpected(Token.CloseBracket);
expr = finishNode(
{
kind: SyntaxKind.ArrayExpression,
elementType: expr,
},
pos
);
expr = {
kind: SyntaxKind.ArrayExpression,
elementType: expr,
...finishNode(pos),
};
}
return expr;
@ -605,14 +648,12 @@ export function parse(code: string | SourceFile) {
const target = parseIdentifierOrMemberExpression();
const args = parseOptionalList(ListKind.TemplateArguments, parseExpression);
return finishNode(
{
kind: SyntaxKind.TypeReference,
target,
arguments: args,
},
pos
);
return {
kind: SyntaxKind.TypeReference,
target,
arguments: args,
...finishNode(pos),
};
}
function parseImportStatement(): ImportStatementNode {
@ -622,13 +663,11 @@ export function parse(code: string | SourceFile) {
const path = parseStringLiteral();
parseExpected(Token.Semicolon);
return finishNode(
{
kind: SyntaxKind.ImportStatement,
path,
},
pos
);
return {
kind: SyntaxKind.ImportStatement,
path,
...finishNode(pos),
};
}
function parseDecoratorExpression(): DecoratorExpressionNode {
@ -637,14 +676,12 @@ export function parse(code: string | SourceFile) {
const target = parseIdentifierOrMemberExpression();
const args = parseOptionalList(ListKind.DecoratorArguments, parseExpression);
return finishNode(
{
kind: SyntaxKind.DecoratorExpression,
arguments: args,
target,
},
pos
);
return {
kind: SyntaxKind.DecoratorExpression,
arguments: args,
target,
...finishNode(pos),
};
}
function parseIdentifierOrMemberExpression(): IdentifierNode | MemberExpressionNode {
@ -652,14 +689,12 @@ export function parse(code: string | SourceFile) {
while (parseOptional(Token.Dot)) {
const pos = tokenPos();
base = finishNode(
{
kind: SyntaxKind.MemberExpression,
base,
id: parseIdentifier(),
},
pos
);
base = {
kind: SyntaxKind.MemberExpression,
base,
id: parseIdentifier(),
...finishNode(pos),
};
}
return base;
@ -698,44 +733,38 @@ export function parse(code: string | SourceFile) {
parseExpected(Token.OpenParen);
const expr = parseExpression();
parseExpected(Token.CloseParen);
return finishNode(expr, pos);
return { ...expr, ...finishNode(pos) };
}
function parseTupleExpression(): TupleExpressionNode {
const pos = tokenPos();
const values = parseList(ListKind.Tuple, parseExpression);
return finishNode(
{
kind: SyntaxKind.TupleExpression,
values,
},
pos
);
return {
kind: SyntaxKind.TupleExpression,
values,
...finishNode(pos),
};
}
function parseModelExpression(): ModelExpressionNode {
const pos = tokenPos();
const properties = parseList(ListKind.ModelProperties, parseModelPropertyOrSpread);
return finishNode(
{
kind: SyntaxKind.ModelExpression,
properties,
},
pos
);
return {
kind: SyntaxKind.ModelExpression,
properties,
...finishNode(pos),
};
}
function parseStringLiteral(): StringLiteralNode {
const pos = tokenPos();
const value = tokenValue();
parseExpected(Token.StringLiteral);
return finishNode(
{
kind: SyntaxKind.StringLiteral,
value,
},
pos
);
return {
kind: SyntaxKind.StringLiteral,
value,
...finishNode(pos),
};
}
function parseNumericLiteral(): NumericLiteralNode {
@ -744,27 +773,22 @@ export function parse(code: string | SourceFile) {
const value = Number(text);
parseExpected(Token.NumericLiteral);
return finishNode(
{
kind: SyntaxKind.NumericLiteral,
text,
value,
},
pos
);
return {
kind: SyntaxKind.NumericLiteral,
value,
...finishNode(pos),
};
}
function parseBooleanLiteral(): BooleanLiteralNode {
const pos = tokenPos();
const token = parseExpectedOneOf(Token.TrueKeyword, Token.FalseKeyword);
const value = token == Token.TrueKeyword;
return finishNode(
{
kind: SyntaxKind.BooleanLiteral,
value,
},
pos
);
const value = token === Token.TrueKeyword;
return {
kind: SyntaxKind.BooleanLiteral,
value,
...finishNode(pos),
};
}
function parseIdentifier(message?: string): IdentifierNode {
@ -781,13 +805,11 @@ export function parse(code: string | SourceFile) {
const sv = tokenValue();
nextToken();
return finishNode(
{
kind: SyntaxKind.Identifier,
sv,
},
pos
);
return {
kind: SyntaxKind.Identifier,
sv,
...finishNode(pos),
};
}
// utility functions
@ -803,34 +825,49 @@ export function parse(code: string | SourceFile) {
return scanner.tokenPosition;
}
function tokenEndPos() {
return scanner.position;
}
function nextToken() {
// keep track of the previous token end separately from the current scanner
// position as these will differ when the previous token had trailing
// trivia, and we don't want to squiggle the trivia.
previousTokenEnd = scanner.position;
do {
for (;;) {
scanner.scan();
} while (isTrivia(token()));
if (isTrivia(token())) {
if (options.comments && isComment(token())) {
comments.push({
kind:
token() === Token.SingleLineComment
? SyntaxKind.LineComment
: SyntaxKind.BlockComment,
pos: tokenPos(),
end: tokenEndPos(),
});
}
} else {
break;
}
}
}
function createMissingIdentifier(): IdentifierNode {
missingIdentifierCounter++;
return finishNode(
{
kind: SyntaxKind.Identifier,
sv: "<missing identifier>" + missingIdentifierCounter,
},
tokenPos()
);
return {
kind: SyntaxKind.Identifier,
sv: "<missing identifier>" + missingIdentifierCounter,
...finishNode(tokenPos()),
};
}
function finishNode<T>(o: T, pos: number): T & TextRange & { flags: NodeFlags } {
function finishNode(pos: number): TextRange & { flags: NodeFlags } {
const flags = parseErrorInNextFinishedNode ? NodeFlags.ThisNodeHasError : NodeFlags.None;
parseErrorInNextFinishedNode = false;
return {
...o,
pos,
end: previousTokenEnd,
flags,
@ -931,7 +968,7 @@ export function parse(code: string | SourceFile) {
* open token is present. Otherwise, return an empty list.
*/
function parseOptionalList<T>(kind: SurroundedListKind, parseItem: ParseListItem<T>): T[] {
return token() == kind.open ? parseList(kind, parseItem) : [];
return token() === kind.open ? parseList(kind, parseItem) : [];
}
function parseOptionalDelimiter(kind: ListKind) {
@ -953,7 +990,7 @@ export function parse(code: string | SourceFile) {
function parseEmptyStatement(): EmptyStatementNode {
const pos = tokenPos();
parseExpected(Token.Semicolon);
return finishNode({ kind: SyntaxKind.EmptyStatement }, pos);
return { kind: SyntaxKind.EmptyStatement, ...finishNode(pos) };
}
function parseInvalidStatement(): InvalidStatementNode {
@ -972,7 +1009,7 @@ export function parse(code: string | SourceFile) {
);
error("Statement expected.", { pos, end: previousTokenEnd });
return finishNode({ kind: SyntaxKind.InvalidStatement }, pos);
return { kind: SyntaxKind.InvalidStatement, ...finishNode(pos) };
}
function error(message: string, target?: TextRange & { realPos?: number }) {
@ -990,18 +1027,15 @@ export function parse(code: string | SourceFile) {
if (realPositionOfLastError === realPos) {
return;
}
realPositionOfLastError = realPos;
parseErrorInNextFinishedNode = true;
reportDiagnostic(message, location);
const diagnostic = createDiagnostic(message, location);
reportDiagnostic(diagnostic);
}
function reportDiagnostic(
message: Message | string,
target: DiagnosticTarget,
args?: (string | number)[]
) {
const diagnostic = createDiagnostic(message, target, args);
function reportDiagnostic(diagnostic: Diagnostic) {
if (diagnostic.severity === "error") {
parseErrorInNextFinishedNode = true;
}
parseDiagnostics.push(diagnostic);
}
@ -1121,9 +1155,20 @@ export function visitChildren<T>(node: Node, cb: NodeCb<T>): T | undefined {
visitNode(cb, node.id) ||
visitEach(cb, node.templateParameters) ||
visitEach(cb, node.heritage) ||
visitNode(cb, node.assignment) ||
visitEach(cb, node.properties)
);
case SyntaxKind.EnumStatement:
return (
visitEach(cb, node.decorators) || visitNode(cb, node.id) || visitEach(cb, node.members)
);
case SyntaxKind.EnumMember:
return visitEach(cb, node.decorators) || visitNode(cb, node.id) || visitNode(cb, node.value);
case SyntaxKind.AliasStatement:
return (
visitNode(cb, node.id) ||
visitEach(cb, node.templateParameters) ||
visitNode(cb, node.value)
);
case SyntaxKind.NamedImport:
return visitNode(cb, node.id);
case SyntaxKind.TypeReference:

Просмотреть файл

@ -1,8 +1,9 @@
import { dirname, extname, isAbsolute, join, resolve } from "path";
import { dirname, extname, isAbsolute, resolve } from "path";
import resolveModule from "resolve";
import { createBinder, createSymbolTable } from "./binder.js";
import { createChecker } from "./checker.js";
import { createSourceFile, DiagnosticError, throwDiagnostic } from "./diagnostics.js";
import { Checker, createChecker } from "./checker.js";
import { createDiagnostic, createSourceFile, DiagnosticTarget, NoTarget } from "./diagnostics.js";
import { Message } from "./messages.js";
import { CompilerOptions } from "./options.js";
import { parse } from "./parser.js";
import {
@ -10,14 +11,18 @@ import {
CompilerHost,
DecoratorExpressionNode,
DecoratorSymbol,
Diagnostic,
IdentifierNode,
LiteralType,
ModelStatementNode,
ModelType,
NamespaceStatementNode,
Sym,
SymbolTable,
SyntaxKind,
Type,
} from "./types.js";
import { doIO, loadFile } from "./util.js";
export interface Program {
compilerOptions: CompilerOptions;
@ -25,46 +30,73 @@ export interface Program {
sourceFiles: ADLScriptNode[];
literalTypes: Map<string | number | boolean, LiteralType>;
host: CompilerHost;
checker?: ReturnType<typeof createChecker>;
checker?: Checker;
readonly diagnostics: readonly Diagnostic[];
evalAdlScript(adlScript: string, filePath?: string): void;
onBuild(cb: (program: Program) => void): Promise<void> | void;
getOption(key: string): string | undefined;
executeModelDecorators(type: ModelType): void;
executeDecorators(type: Type): void;
executeDecorator(node: DecoratorExpressionNode, program: Program, type: Type): void;
stateSet(key: Symbol): Set<any>;
stateMap(key: Symbol): Map<any, any>;
hasError(): boolean;
reportDiagnostic(
message: Message | string,
target: DiagnosticTarget | typeof NoTarget,
args?: (string | number)[]
): void;
reportDiagnostic(diagnostic: Diagnostic): void;
reportDiagnostics(diagnostics: Diagnostic[]): void;
reportDuplicateSymbols(symbols: SymbolTable): void;
}
export async function createProgram(
host: CompilerHost,
options: CompilerOptions
mainFile: string,
options: CompilerOptions = {}
): Promise<Program> {
const buildCbs: any = [];
const stateMaps = new Map<Symbol, Map<any, any>>();
const stateSets = new Map<Symbol, Set<any>>();
const diagnostics: Diagnostic[] = [];
const seenSourceFiles = new Set<string>();
const duplicateSymbols = new Set<Sym>();
let error = false;
const program: Program = {
compilerOptions: options || {},
compilerOptions: options,
globalNamespace: createGlobalNamespace(),
sourceFiles: [],
literalTypes: new Map(),
host,
diagnostics,
evalAdlScript,
executeModelDecorators,
executeDecorators,
executeDecorator,
getOption,
stateMap,
stateSet,
reportDiagnostic,
reportDiagnostics,
reportDuplicateSymbols,
hasError() {
return error;
},
onBuild(cb) {
buildCbs.push(cb);
},
};
let virtualFileCount = 0;
const binder = createBinder();
const binder = createBinder(program);
if (!options?.nostdlib) {
await loadStandardLibrary(program);
}
await loadMain(options);
await loadMain(mainFile, options);
const checker = (program.checker = createChecker(program));
program.checker.checkProgram(program);
@ -124,14 +156,16 @@ export async function createProgram(
function executeDecorator(dec: DecoratorExpressionNode, program: Program, type: Type) {
if (dec.target.kind !== SyntaxKind.Identifier) {
throwDiagnostic("Decorator must be identifier", dec);
program.reportDiagnostic("Decorator must be identifier", dec);
return;
}
const decName = dec.target.sv;
const args = dec.arguments.map((a) => toJSON(checker.getTypeForNode(a)));
const decBinding = program.globalNamespace.locals!.get(decName) as DecoratorSymbol;
if (!decBinding) {
throwDiagnostic(`Can't find decorator ${decName}`, dec);
program.reportDiagnostic(`Can't find decorator ${decName}`, dec);
return;
}
const decFn = decBinding.value;
decFn(program, type, ...args);
@ -142,7 +176,7 @@ export async function createProgram(
* just the raw type objects, but literals are treated specially.
*/
function toJSON(type: Type): Type | string | number | boolean {
if ("value" in type) {
if (type.kind === "Number" || type.kind === "String" || type.kind === "Boolean") {
return type.value;
}
@ -155,36 +189,40 @@ export async function createProgram(
}
}
async function loadDirectory(rootDir: string) {
const dir = await host.readDir(rootDir);
for (const entry of dir) {
if (entry.isFile()) {
const path = join(rootDir, entry.name);
if (entry.name.endsWith(".js")) {
await loadJsFile(path);
} else if (entry.name.endsWith(".adl")) {
await loadAdlFile(path);
}
}
}
async function loadDirectory(dir: string, diagnosticTarget?: DiagnosticTarget) {
const pkgJsonPath = resolve(dir, "package.json");
let [pkg] = await loadFile(host.readFile, pkgJsonPath, JSON.parse, program.reportDiagnostic, {
allowFileNotFound: true,
diagnosticTarget,
});
const mainFile = resolve(dir, pkg?.adlMain ?? "main.adl");
await loadAdlFile(mainFile, diagnosticTarget);
}
async function loadAdlFile(path: string) {
async function loadAdlFile(path: string, diagnosticTarget?: DiagnosticTarget) {
if (seenSourceFiles.has(path)) {
return;
}
seenSourceFiles.add(path);
const contents = await host.readFile(path);
if (!contents) {
throw new Error("Couldn't load ADL file " + path);
}
const contents = await doIO(host.readFile, path, program.reportDiagnostic, {
diagnosticTarget,
});
await evalAdlScript(contents, path);
if (contents) {
await evalAdlScript(contents, path);
}
}
async function loadJsFile(path: string) {
const exports = await host.getJsImport(path);
async function loadJsFile(path: string, diagnosticTarget: DiagnosticTarget) {
const exports = await doIO(host.getJsImport, path, program.reportDiagnostic, {
diagnosticTarget,
jsDiagnosticTarget: { file: createSourceFile("", path), pos: 0, end: 0 },
});
if (!exports) {
return;
}
for (const match of Object.keys(exports)) {
// bind JS files early since this is the only work
@ -212,13 +250,9 @@ export async function createProgram(
const unparsedFile = createSourceFile(adlScript, filePath);
const sourceFile = parse(unparsedFile);
// We don't attempt to evaluate yet when there are parse errors.
if (sourceFile.parseDiagnostics.length > 0) {
throw new DiagnosticError(sourceFile.parseDiagnostics);
}
program.reportDiagnostics(sourceFile.parseDiagnostics);
program.sourceFiles.push(sourceFile);
binder.bindSourceFile(program, sourceFile);
binder.bindSourceFile(sourceFile);
await evalImports(sourceFile);
}
@ -240,7 +274,8 @@ export async function createProgram(
target = await resolveModuleSpecifier(path, basedir);
} catch (e) {
if (e.code === "MODULE_NOT_FOUND") {
throwDiagnostic(`Couldn't find library "${path}"`, stmt);
program.reportDiagnostic(`Couldn't find library "${path}"`, stmt);
continue;
} else {
throw e;
}
@ -250,14 +285,13 @@ export async function createProgram(
const ext = extname(target);
if (ext === "") {
// look for a main.adl
await loadAdlFile(join(target, "main.adl"));
} else if (ext === ".js") {
await loadJsFile(target);
await loadDirectory(target, stmt);
} else if (ext === ".js" || ext === ".mjs") {
await loadJsFile(target, stmt);
} else if (ext === ".adl") {
await loadAdlFile(target);
await loadAdlFile(target, stmt);
} else {
throwDiagnostic(
program.reportDiagnostic(
"Import paths must reference either a directory, a .adl file, or .js file",
stmt
);
@ -313,7 +347,13 @@ export async function createProgram(
host
.realpath(path)
.then((p) => cb(null, p))
.catch((e) => cb(e));
.catch((e) => {
if (e.code === "ENOENT" || e.code === "ENOTDIR") {
cb(null, path);
} else {
cb(e);
}
});
},
packageFilter(pkg) {
// this lets us follow node resolve semantics more-or-less exactly
@ -326,8 +366,7 @@ export async function createProgram(
if (err) {
rejectP(err);
} else if (!resolved) {
// I don't know when this happens
rejectP(new Error("Couldn't resolve module"));
rejectP(new Error("BUG: Module resolution succeeded but didn't return a value."));
} else {
resolveP(resolved);
}
@ -336,15 +375,12 @@ export async function createProgram(
});
}
async function loadMain(options: CompilerOptions) {
if (!options.mainFile) {
throw new Error("Must specify a main file");
async function loadMain(mainFile: string, options: CompilerOptions) {
const mainPath = resolve(host.getCwd(), mainFile);
const mainStat = await doIO(host.stat, mainPath, program.reportDiagnostic);
if (!mainStat) {
return;
}
const mainPath = resolve(host.getCwd(), options.mainFile);
const mainStat = await host.stat(mainPath);
if (mainStat.isDirectory()) {
await loadDirectory(mainPath);
} else {
@ -355,8 +391,69 @@ export async function createProgram(
function getOption(key: string): string | undefined {
return (options.miscOptions || {})[key];
}
function stateMap(key: Symbol): Map<any, any> {
let m = stateMaps.get(key);
if (!m) {
m = new Map();
stateMaps.set(key, m);
}
return m;
}
function stateSet(key: Symbol): Set<any> {
let s = stateSets.get(key);
if (!s) {
s = new Set();
stateSets.set(key, s);
}
return s;
}
function reportDiagnostic(diagnostic: Diagnostic): void;
function reportDiagnostic(
message: Message | string,
target: DiagnosticTarget | typeof NoTarget,
args?: (string | number)[]
): void;
function reportDiagnostic(
diagnostic: Message | string | Diagnostic,
target?: DiagnosticTarget | typeof NoTarget,
args?: (string | number)[]
): void {
if (typeof diagnostic === "string" || "text" in diagnostic) {
diagnostic = createDiagnostic(diagnostic, target!, args);
}
if (diagnostic.severity === "error") {
error = true;
}
diagnostics.push(diagnostic);
}
function reportDiagnostics(newDiagnostics: Diagnostic[]) {
for (const diagnostic of newDiagnostics) {
reportDiagnostic(diagnostic);
}
}
function reportDuplicateSymbols(symbols: SymbolTable) {
for (const symbol of symbols.duplicates) {
if (!duplicateSymbols.has(symbol)) {
duplicateSymbols.add(symbol);
reportDiagnostic("Duplicate name: " + symbol.name, symbol);
}
}
}
}
export async function compile(rootDir: string, host: CompilerHost, options?: CompilerOptions) {
const program = await createProgram(host, { mainFile: rootDir, ...options });
export async function compile(
mainFile: string,
host: CompilerHost,
options?: CompilerOptions
): Promise<Program> {
return await createProgram(host, mainFile, options);
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -10,6 +10,8 @@ export interface BaseType {
export type Type =
| ModelType
| ModelTypeProperty
| EnumType
| EnumMemberType
| TemplateParameterType
| NamespaceType
| OperationType
@ -26,6 +28,10 @@ export interface IntrinsicType extends BaseType {
name: string;
}
export interface ErrorType extends IntrinsicType {
name: "ErrorType";
}
export interface ModelType extends BaseType {
kind: "Model";
name: string;
@ -35,7 +41,6 @@ export interface ModelType extends BaseType {
baseModels: ModelType[];
templateArguments?: Type[];
templateNode?: Node;
assignmentType?: Type;
}
export interface ModelTypeProperty {
@ -49,12 +54,28 @@ export interface ModelTypeProperty {
optional: boolean;
}
export interface EnumType extends BaseType {
kind: "Enum";
name: string;
node: EnumStatementNode;
namespace?: NamespaceType;
members: EnumMemberType[];
}
export interface EnumMemberType extends BaseType {
kind: "EnumMember";
name: string;
enum: EnumType;
node: EnumMemberNode;
value?: string | number;
}
export interface OperationType {
kind: "Operation";
node: OperationStatementNode;
name: string;
namespace?: NamespaceType;
parameters?: ModelType;
parameters: ModelType;
returnType: Type;
}
@ -166,6 +187,9 @@ export enum SyntaxKind {
ModelExpression,
ModelProperty,
ModelSpreadProperty,
EnumStatement,
EnumMember,
AliasStatement,
UnionExpression,
IntersectionExpression,
TupleExpression,
@ -177,6 +201,8 @@ export enum SyntaxKind {
TemplateParameterDeclaration,
EmptyStatement,
InvalidStatement,
LineComment,
BlockComment,
}
export interface BaseNode extends TextRange {
@ -190,12 +216,21 @@ export type Node =
| ModelPropertyNode
| OperationStatementNode
| NamedImportNode
| ModelPropertyNode
| EnumMemberNode
| ModelSpreadPropertyNode
| DecoratorExpressionNode
| Statement
| Expression;
export type Comment = LineComment | BlockComment;
export interface LineComment extends TextRange {
kind: SyntaxKind.LineComment;
}
export interface BlockComment extends TextRange {
kind: SyntaxKind.BlockComment;
}
export interface ADLScriptNode extends BaseNode {
kind: SyntaxKind.ADLScript;
statements: Statement[];
@ -206,6 +241,7 @@ export interface ADLScriptNode extends BaseNode {
namespaces: NamespaceStatementNode[]; // list of namespaces in this file (initialized during binding)
locals: SymbolTable;
usings: UsingStatementNode[];
comments: Comment[];
parseDiagnostics: Diagnostic[];
}
@ -214,6 +250,8 @@ export type Statement =
| ModelStatementNode
| NamespaceStatementNode
| UsingStatementNode
| EnumStatementNode
| AliasStatementNode
| OperationStatementNode
| EmptyStatementNode
| InvalidStatementNode;
@ -227,9 +265,15 @@ export type Declaration =
| ModelStatementNode
| NamespaceStatementNode
| OperationStatementNode
| TemplateParameterDeclarationNode;
| TemplateParameterDeclarationNode
| EnumStatementNode
| AliasStatementNode;
export type ScopeNode = NamespaceStatementNode | ModelStatementNode | ADLScriptNode;
export type ScopeNode =
| NamespaceStatementNode
| ModelStatementNode
| AliasStatementNode
| ADLScriptNode;
export interface ImportStatementNode extends BaseNode {
kind: SyntaxKind.ImportStatement;
@ -298,14 +342,35 @@ export interface OperationStatementNode extends BaseNode, DeclarationNode {
export interface ModelStatementNode extends BaseNode, DeclarationNode {
kind: SyntaxKind.ModelStatement;
id: IdentifierNode;
properties?: (ModelPropertyNode | ModelSpreadPropertyNode)[];
properties: (ModelPropertyNode | ModelSpreadPropertyNode)[];
heritage: ReferenceExpression[];
assignment?: Expression;
templateParameters: TemplateParameterDeclarationNode[];
locals?: SymbolTable;
decorators: DecoratorExpressionNode[];
}
export interface EnumStatementNode extends BaseNode, DeclarationNode {
kind: SyntaxKind.EnumStatement;
id: IdentifierNode;
members: EnumMemberNode[];
decorators: DecoratorExpressionNode[];
}
export interface EnumMemberNode extends BaseNode {
kind: SyntaxKind.EnumMember;
id: IdentifierNode | StringLiteralNode;
value?: StringLiteralNode | NumericLiteralNode;
decorators: DecoratorExpressionNode[];
}
export interface AliasStatementNode extends BaseNode, DeclarationNode {
kind: SyntaxKind.AliasStatement;
id: IdentifierNode;
value: Expression;
templateParameters: TemplateParameterDeclarationNode[];
locals?: SymbolTable;
}
export interface InvalidStatementNode extends BaseNode {
kind: SyntaxKind.InvalidStatement;
}
@ -446,7 +511,7 @@ export interface SourceLocation extends TextRange {
file: SourceFile;
}
export interface Diagnostic extends SourceLocation {
export interface Diagnostic extends Partial<SourceLocation> {
message: string;
code?: number;
severity: "warning" | "error";
@ -460,7 +525,7 @@ export interface Dirent {
export interface CompilerHost {
// read a utf-8 encoded file
readFile(path: string): Promise<string | undefined>;
readFile(path: string): Promise<string>;
// read the contents of a directory
readDir(path: string): Promise<Dirent[]>;

Просмотреть файл

@ -2,8 +2,15 @@ import fs from "fs";
import { readdir, readFile, realpath, stat, writeFile } from "fs/promises";
import { join, resolve } from "path";
import { fileURLToPath, pathToFileURL, URL } from "url";
import { createDiagnostic, DiagnosticError } from "./diagnostics.js";
import { CompilerHost, Diagnostic, Sym, SymbolTable } from "./types.js";
import {
createDiagnostic,
createSourceFile,
DiagnosticHandler,
DiagnosticTarget,
Message,
NoTarget,
} from "./diagnostics.js";
import { CompilerHost, SourceFile } from "./types.js";
export const adlVersion = getVersion();
@ -13,33 +20,99 @@ function getVersion(): string {
return packageJson.version;
}
export function reportDuplicateSymbols(symbols: SymbolTable) {
let reported = new Set<Sym>();
let diagnostics: Diagnostic[] = [];
for (const symbol of symbols.duplicates) {
report(symbol);
}
if (diagnostics.length > 0) {
// TODO: We're now reporting all duplicates up to the binding of the first file
// that introduced one, but still bailing the compilation rather than
// recovering and reporting other issues including the possibility of more
// duplicates.
//
// That said, decorators are entered into the global symbol table before
// any source file is bound and therefore this will include all duplicate
// decorator implementations.
throw new DiagnosticError(diagnostics);
}
function report(symbol: Sym) {
if (!reported.has(symbol)) {
reported.add(symbol);
const diagnostic = createDiagnostic("Duplicate name: " + symbol.name, symbol);
diagnostics.push(diagnostic);
export function deepFreeze<T>(value: T): T {
if (Array.isArray(value)) {
value.map(deepFreeze);
} else if (typeof value === "object") {
for (const prop in value) {
deepFreeze(value[prop]);
}
}
return Object.freeze(value);
}
export function deepClone<T>(value: T): T {
if (Array.isArray(value)) {
return value.map(deepClone) as any;
}
if (typeof value === "object") {
const obj: any = {};
for (const prop in value) {
obj[prop] = deepClone(value[prop]);
}
return obj;
}
return value;
}
export interface FileHandlingOptions {
allowFileNotFound?: boolean;
diagnosticTarget?: DiagnosticTarget;
jsDiagnosticTarget?: DiagnosticTarget;
}
export async function doIO<T>(
action: (path: string) => Promise<T>,
path: string,
reportDiagnostic: DiagnosticHandler,
options?: FileHandlingOptions
): Promise<T | undefined> {
let result;
try {
result = await action(path);
} catch (e) {
let diagnostic;
let target = options?.diagnosticTarget ?? NoTarget;
// blame the JS file, not the ADL import statement for JS syntax errors.
if (e instanceof SyntaxError && options?.jsDiagnosticTarget) {
target = options.jsDiagnosticTarget;
}
switch (e.code) {
case "ENOENT":
if (options?.allowFileNotFound) {
return undefined;
}
diagnostic = createDiagnostic(Message.FileNotFound, target, [path]);
break;
default:
diagnostic = createDiagnostic(e.message, target);
break;
}
reportDiagnostic(diagnostic);
return undefined;
}
return result;
}
export async function loadFile<T>(
read: (path: string) => Promise<string>,
path: string,
load: (contents: string) => T,
reportDiagnostic: DiagnosticHandler,
options?: FileHandlingOptions
): Promise<[T | undefined, SourceFile]> {
const contents = await doIO(read, path, reportDiagnostic, options);
if (!contents) {
return [undefined, createSourceFile("", path)];
}
const file = createSourceFile(contents, path);
let data: T;
try {
data = load(contents);
} catch (e) {
reportDiagnostic({ message: e.message, severity: "error", file });
return [undefined, file];
}
return [data, file];
}
export const NodeHost: CompilerHost = {
@ -51,7 +124,7 @@ export const NodeHost: CompilerHost = {
getJsImport: (path: string) => import(pathToFileURL(path).href),
getLibDirs() {
const rootDir = this.getExecutionRoot();
return [join(rootDir, "lib"), join(rootDir, "dist/lib")];
return [join(rootDir, "lib")];
},
stat(path: string) {
return stat(path);

Просмотреть файл

@ -0,0 +1,121 @@
import { readFile } from "fs/promises";
import { basename, extname, join } from "path";
import { Message } from "../compiler/diagnostics.js";
import { Diagnostic } from "../compiler/types.js";
import { deepClone, deepFreeze, loadFile } from "../compiler/util.js";
import { ConfigValidator } from "./config-validator.js";
import { ADLConfig } from "./types.js";
const configFilenames = [".adlrc.yaml", ".adlrc.yml", ".adlrc.json", "package.json"];
const defaultConfig: ADLConfig = deepFreeze({
plugins: [],
diagnostics: [],
emitters: {},
lint: {
extends: [],
rules: {},
},
});
/**
* Load ADL Configuration if present.
* @param directoryPath Current working directory where the config should be.
*/
export async function loadADLConfigInDir(directoryPath: string): Promise<ADLConfig> {
for (const filename of configFilenames) {
const filePath = join(directoryPath, filename);
const config = await loadADLConfigFile(filePath);
if (
config.diagnostics.length === 1 &&
config.diagnostics[0].code === Message.FileNotFound.code
) {
continue;
}
return config;
}
return deepClone(defaultConfig);
}
/**
* Load given file as an adl configuration
*/
export async function loadADLConfigFile(filePath: string): Promise<ADLConfig> {
switch (extname(filePath)) {
case ".json":
if (basename(filePath) === "package.json") {
return loadPackageJSONConfigFile(filePath);
}
return loadJSONConfigFile(filePath);
case ".yaml":
case ".yml":
return loadYAMLConfigFile(filePath);
default:
// This is not a diagnostic because the compiler only tries the
// well-known config file names.
throw new RangeError("Config file must have .yaml, .yml, or .json extension.");
}
}
export async function loadPackageJSONConfigFile(filePath: string): Promise<ADLConfig> {
return await loadConfigFile(filePath, (content) => JSON.parse(content).adl ?? {});
}
export async function loadJSONConfigFile(filePath: string): Promise<ADLConfig> {
return await loadConfigFile(filePath, JSON.parse);
}
export async function loadYAMLConfigFile(filePath: string): Promise<ADLConfig> {
// Lazy load.
const jsyaml = await import("js-yaml");
return await loadConfigFile(filePath, jsyaml.load);
}
const configValidator = new ConfigValidator();
async function loadConfigFile(
filePath: string,
loadData: (content: string) => any
): Promise<ADLConfig> {
const diagnostics: Diagnostic[] = [];
const reportDiagnostic = (d: Diagnostic) => diagnostics.push(d);
let [data, file] = await loadFile(
(path) => readFile(path, "utf-8"),
filePath,
loadData,
reportDiagnostic
);
if (data) {
configValidator.validateConfig(data, file, reportDiagnostic);
}
if (!data || diagnostics.length > 0) {
// NOTE: Don't trust the data if there are errors and use default
// config. Otherwise, we may return an object that does not conform to
// ADLConfig's typing.
data = deepClone(defaultConfig);
} else {
mergeDefaults(data, defaultConfig);
}
data.filename = filePath;
data.diagnostics = diagnostics;
return data;
}
/**
* Recursively add properties from defaults that are not present in target.
*/
function mergeDefaults(target: any, defaults: any) {
for (const prop in defaults) {
const value = target[prop];
if (value === undefined) {
target[prop] = deepClone(defaults[prop]);
} else if (typeof value === "object" && typeof defaults[prop] === "object") {
mergeDefaults(value, defaults[prop]);
}
}
}

Просмотреть файл

@ -0,0 +1,46 @@
import { JSONSchemaType } from "ajv";
import { ADLRawConfig } from "./types.js";
export const ADLConfigJsonSchema: JSONSchemaType<ADLRawConfig> = {
type: "object",
additionalProperties: false,
properties: {
plugins: {
type: "array",
nullable: true,
items: {
type: "string",
},
},
lint: {
type: "object",
nullable: true,
additionalProperties: false,
properties: {
extends: {
type: "array",
nullable: true,
items: {
type: "string",
},
},
rules: {
type: "object",
nullable: true,
required: [],
additionalProperties: {
oneOf: [{ type: "string", enum: ["on", "off"] }, { type: "object" }],
},
},
},
},
emitters: {
type: "object",
nullable: true,
required: [],
additionalProperties: {
type: "boolean",
},
},
},
};

Просмотреть файл

@ -0,0 +1,50 @@
import Ajv, { ErrorObject } from "ajv";
import { compilerAssert, DiagnosticHandler } from "../compiler/diagnostics.js";
import { Diagnostic, SourceFile } from "../compiler/types.js";
import { ADLConfigJsonSchema } from "./config-schema.js";
import { ADLRawConfig } from "./types.js";
export class ConfigValidator {
private ajv = new Ajv({
strict: true,
});
/**
* Validate the config is valid
* @param config Configuration
* @param file @optional file for errors tracing.
* @returns Validation
*/
public validateConfig(
config: ADLRawConfig,
file: SourceFile,
reportDiagnostic: DiagnosticHandler
): void {
const validate = this.ajv.compile(ADLConfigJsonSchema);
const valid = validate(config);
compilerAssert(
!valid || !validate.errors,
"There should be errors reported if the config file is not valid."
);
for (const error of validate.errors ?? []) {
const diagnostic = ajvErrorToDiagnostic(error, file);
reportDiagnostic(diagnostic);
}
}
}
const IGNORED_AJV_PARAMS = new Set(["type", "errors"]);
function ajvErrorToDiagnostic(error: ErrorObject, file: SourceFile): Diagnostic {
const messageLines = [`Schema violation: ${error.message} (${error.instancePath || "/"})`];
for (const [name, value] of Object.entries(error.params).filter(
([name]) => !IGNORED_AJV_PARAMS.has(name)
)) {
const formattedValue = Array.isArray(value) ? [...new Set(value)].join(", ") : value;
messageLines.push(` ${name}: ${formattedValue}`);
}
const message = messageLines.join("\n");
return { message, severity: "error", file };
}

Просмотреть файл

@ -0,0 +1,2 @@
export * from "./config-loader.js";
export * from "./types.js";

Просмотреть файл

@ -0,0 +1,36 @@
import { Diagnostic } from "../compiler";
/**
* Represent the normalized user configuration.
*/
export interface ADLConfig {
/**
* Path to the config file used to create this configuration.
*/
filename?: string;
/**
* Diagnostics reported while loading the configuration
*/
diagnostics: Diagnostic[];
plugins: string[];
lint: ADLLintConfig;
emitters: Record<string, boolean>;
}
export type RuleValue = "on" | "off" | {};
export interface ADLLintConfig {
extends: string[];
rules: Record<string, RuleValue>;
}
/**
* Represent the configuration that can be provided in a config file.
*/
export interface ADLRawConfig {
plugins?: string[];
lint?: Partial<ADLLintConfig>;
emitters?: Record<string, boolean>;
}

Просмотреть файл

@ -0,0 +1,33 @@
import { Parser, SupportLanguage } from "prettier";
import { Node } from "../compiler/types.js";
import { parse } from "./parser.js";
import { ADLPrinter } from "./print/index.js";
export const defaultOptions = {};
export const languages: SupportLanguage[] = [
{
name: "ADL",
parsers: ["adl"],
extensions: [".adl"],
vscodeLanguageIds: ["adl"],
},
];
const ADLParser: Parser = {
parse,
astFormat: "adl-format",
locStart(node: Node) {
return node.pos;
},
locEnd(node: Node) {
return node.end;
},
};
export const parsers = {
adl: ADLParser,
};
export const printers = {
"adl-format": ADLPrinter,
};

Просмотреть файл

@ -0,0 +1,27 @@
import { Parser, ParserOptions } from "prettier";
import { parse as adlParse } from "../compiler/parser.js";
import { ADLScriptNode, Diagnostic } from "../compiler/types.js";
export function parse(
text: string,
parsers: { [parserName: string]: Parser },
opts: ParserOptions & { parentParser?: string }
): ADLScriptNode {
const result = adlParse(text, { comments: true });
const errors = result.parseDiagnostics.filter((x) => x.severity === "error");
if (errors.length > 0) {
throw new PrettierParserError(errors[0]);
}
return result;
}
export class PrettierParserError extends Error {
public loc: { start: number; end: number };
public constructor(public readonly error: Diagnostic) {
super(error.message);
this.loc = {
start: error.pos ?? 0,
end: error.end ?? 0,
};
}
}

Просмотреть файл

@ -0,0 +1,2 @@
export * from "./printer.js";
export * from "./types.js";

Просмотреть файл

@ -0,0 +1,637 @@
import prettier, { Doc, FastPath, Printer } from "prettier";
import {
ADLScriptNode,
AliasStatementNode,
BlockComment,
Comment,
DecoratorExpressionNode,
EnumMemberNode,
EnumStatementNode,
IntersectionExpressionNode,
ModelExpressionNode,
ModelPropertyNode,
ModelSpreadPropertyNode,
ModelStatementNode,
NamespaceStatementNode,
Node,
NumericLiteralNode,
OperationStatementNode,
Statement,
StringLiteralNode,
SyntaxKind,
TextRange,
TypeReferenceNode,
UnionExpressionNode,
} from "../../compiler/types.js";
import { ADLPrettierOptions, DecorableNode, PrettierChildPrint } from "./types.js";
const {
align,
breakParent,
concat,
group,
hardline,
ifBreak,
indent,
join,
line,
softline,
} = prettier.doc.builders;
const { isNextLineEmpty } = prettier.util;
const { replaceNewlinesWithLiterallines } = prettier.doc.utils as any;
export const ADLPrinter: Printer<Node> = {
print: printADL,
canAttachComment: canAttachComment,
printComment: printComment,
};
export function printADL(
// Path to the AST node to print
path: FastPath<Node>,
options: ADLPrettierOptions,
print: PrettierChildPrint
): prettier.Doc {
const node: Node = path.getValue();
switch (node.kind) {
// Root
case SyntaxKind.ADLScript:
return printStatementSequence(path as FastPath<ADLScriptNode>, options, print, "statements");
// Statements
case SyntaxKind.ImportStatement:
return concat([`import "${node.path.value}";`]);
case SyntaxKind.UsingStatement:
return concat([`using "`, path.call(print, "name"), `";`]);
case SyntaxKind.OperationStatement:
return printOperationStatement(path as FastPath<OperationStatementNode>, options, print);
case SyntaxKind.NamespaceStatement:
return printNamespaceStatement(path as FastPath<NamespaceStatementNode>, options, print);
case SyntaxKind.ModelStatement:
return printModelStatement(path as FastPath<ModelStatementNode>, options, print);
case SyntaxKind.AliasStatement:
return printAliasStatement(path as FastPath<AliasStatementNode>, options, print);
case SyntaxKind.EnumStatement:
return printEnumStatement(path as FastPath<EnumStatementNode>, options, print);
// Others.
case SyntaxKind.Identifier:
return node.sv;
case SyntaxKind.StringLiteral:
return printStringLiteral(path as FastPath<StringLiteralNode>, options);
case SyntaxKind.NumericLiteral:
return printNumberLiteral(path as FastPath<NumericLiteralNode>, options);
case SyntaxKind.ModelExpression:
return printModelExpression(path as FastPath<ModelExpressionNode>, options, print);
case SyntaxKind.ModelProperty:
return printModelProperty(path as FastPath<ModelPropertyNode>, options, print);
case SyntaxKind.DecoratorExpression:
return printDecorator(path as FastPath<DecoratorExpressionNode>, options, print);
case SyntaxKind.UnionExpression:
return printUnion(path as FastPath<UnionExpressionNode>, options, print);
case SyntaxKind.IntersectionExpression:
return printIntersection(path as FastPath<IntersectionExpressionNode>, options, print);
case SyntaxKind.EnumMember:
return printEnumMember(path as FastPath<EnumMemberNode>, options, print);
case SyntaxKind.TypeReference:
return printTypeReference(path as FastPath<TypeReferenceNode>, options, print);
default:
return getRawText(node, options);
}
}
export function printAliasStatement(
path: FastPath<AliasStatementNode>,
options: ADLPrettierOptions,
print: PrettierChildPrint
) {
const id = path.call(print, "id");
const template = printTemplateParameters(path, options, print, "templateParameters");
return concat(["alias ", id, template, " = ", path.call(print, "value"), ";"]);
}
function printTemplateParameters<T extends Node>(
path: FastPath<T>,
options: ADLPrettierOptions,
print: PrettierChildPrint,
propertyName: keyof T
) {
const value = path.getValue()[propertyName];
if ((value as any).length === 0) {
return "";
}
return concat(["<", join(", ", path.map(print, propertyName)), ">"]);
}
export function canAttachComment(node: Node): boolean {
const kind = node.kind as SyntaxKind;
return Boolean(kind && kind !== SyntaxKind.LineComment && kind !== SyntaxKind.BlockComment);
}
export function printComment(
commentPath: FastPath<Node | Comment>,
options: ADLPrettierOptions
): Doc {
const comment = commentPath.getValue();
switch (comment.kind) {
case SyntaxKind.BlockComment:
return printBlockComment(commentPath as FastPath<BlockComment>, options);
case SyntaxKind.LineComment:
return `${options.originalText.slice(comment.pos, comment.end).trimRight()}`;
default:
throw new Error(`Not a comment: ${JSON.stringify(comment)}`);
}
}
function printBlockComment(commentPath: FastPath<BlockComment>, options: ADLPrettierOptions) {
const comment = commentPath.getValue();
const rawComment = options.originalText.slice(comment.pos + 2, comment.end - 2);
if (isIndentableBlockComment(rawComment)) {
const printed = printIndentableBlockComment(rawComment);
return printed;
}
return concat(["/*", replaceNewlinesWithLiterallines(rawComment), "*/"]);
}
function isIndentableBlockComment(rawComment: string): boolean {
// If the comment has multiple lines and every line starts with a star
// we can fix the indentation of each line. The stars in the `/*` and
// `*/` delimiters are not included in the comment value, so add them
// back first.
const lines = `*${rawComment}*`.split("\n");
return lines.length > 1 && lines.every((line) => line.trim()[0] === "*");
}
function printIndentableBlockComment(rawComment: string): Doc {
const lines = rawComment.split("\n");
return concat([
"/*",
join(
hardline,
lines.map((line, index) =>
index === 0
? line.trimEnd()
: " " + (index < lines.length - 1 ? line.trim() : line.trimStart())
)
),
"*/",
]);
}
export function printDecorators(
path: FastPath<DecorableNode>,
options: object,
print: PrettierChildPrint,
{ tryInline }: { tryInline: boolean }
) {
const node = path.getValue();
if (node.decorators.length === 0) {
return "";
}
const decorators = path.map((x) => concat([print(x as any), ifBreak(line, " ")]), "decorators");
const shouldBreak = tryInline && decorators.length < 3 ? "" : breakParent;
return group(concat([...decorators, shouldBreak]));
}
export function printDecorator(
path: FastPath<DecoratorExpressionNode>,
options: object,
print: PrettierChildPrint
) {
const args = printDecoratorArgs(path, options, print);
return concat(["@", path.call(print, "target"), args]);
}
function printDecoratorArgs(
path: FastPath<DecoratorExpressionNode>,
options: object,
print: PrettierChildPrint
) {
const node = path.getValue();
if (node.arguments.length === 0) {
return "";
}
// So that decorator with single object arguments have ( and { hugging.
// @deco({
// value: "foo"
// })
const shouldHug =
node.arguments.length === 1 && node.arguments[0].kind === SyntaxKind.ModelExpression;
if (shouldHug) {
return concat([
"(",
join(
", ",
path.map((arg) => concat([print(arg)]), "arguments")
),
")",
]);
}
return concat([
"(",
group(
concat([
indent(
join(
", ",
path.map((arg) => concat([softline, print(arg)]), "arguments")
)
),
softline,
])
),
")",
]);
}
export function printEnumStatement(
path: FastPath<EnumStatementNode>,
options: ADLPrettierOptions,
print: PrettierChildPrint
) {
const id = path.call(print, "id");
const decorators = printDecorators(path, options, print, { tryInline: false });
return concat([decorators, "enum ", id, " ", printEnumBlock(path, options, print)]);
}
function printEnumBlock(
path: FastPath<EnumStatementNode>,
options: ADLPrettierOptions,
print: PrettierChildPrint
) {
const node = path.getValue();
if (node.members.length === 0) {
return "{}";
}
return group(
concat([
"{",
indent(
concat([
hardline,
join(
hardline,
path.map((x) => concat([print(x as any), ","]), "members")
),
])
),
hardline,
"}",
])
);
}
export function printEnumMember(
path: FastPath<EnumMemberNode>,
options: ADLPrettierOptions,
print: PrettierChildPrint
) {
const node = path.getValue();
const id = path.call(print, "id");
const value = node.value ? concat([": ", path.call(print, "value")]) : "";
const decorators = printDecorators(path, options, print, { tryInline: true });
return concat([decorators, id, value]);
}
/**
* Handle printing an intersection node.
* @example `Foo & Bar` or `{foo: string} & {bar: string}`
*
* @param path Prettier AST Path.
* @param options Prettier options
* @param print Prettier child print callback.
* @returns Prettier document.
*/
export function printIntersection(
path: FastPath<IntersectionExpressionNode>,
options: object,
print: PrettierChildPrint
) {
const node = path.getValue();
const types = path.map(print, "options");
const result: (prettier.Doc | string)[] = [];
let wasIndented = false;
for (let i = 0; i < types.length; ++i) {
if (i === 0) {
result.push(types[i]);
} else if (isModelNode(node.options[i - 1]) && isModelNode(node.options[i])) {
// If both are objects, don't indent
result.push(concat([" & ", wasIndented ? indent(types[i]) : types[i]]));
} else if (!isModelNode(node.options[i - 1]) && !isModelNode(node.options[i])) {
// If no object is involved, go to the next line if it breaks
result.push(indent(concat([" &", line, types[i]])));
} else {
// If you go from object to non-object or vis-versa, then inline it
if (i > 1) {
wasIndented = true;
}
result.push(" & ", i > 1 ? indent(types[i]) : types[i]);
}
}
return group(concat(result));
}
function isModelNode(node: Node) {
return node.kind === SyntaxKind.ModelExpression;
}
export function printModelExpression(
path: FastPath<ModelExpressionNode>,
options: ADLPrettierOptions,
print: PrettierChildPrint
) {
const inBlock = isModelExpressionInBlock(path);
if (inBlock) {
return group(printModelPropertiesBlock(path, options, print));
} else {
return group(
concat([
indent(
join(
", ",
path.map((arg) => concat([softline, print(arg)]), "properties")
)
),
softline,
])
);
}
}
export function printModelStatement(
path: FastPath<ModelStatementNode>,
options: ADLPrettierOptions,
print: PrettierChildPrint
) {
const node = path.getValue();
const id = path.call(print, "id");
const heritage =
node.heritage.length > 0 ? concat(["extends ", path.map(print, "heritage")[0], " "]) : "";
const generic = printTemplateParameters(path, options, print, "templateParameters");
return concat([
printDecorators(path, options, print, { tryInline: false }),
"model ",
id,
generic,
" ",
heritage,
printModelPropertiesBlock(path, options, print),
]);
}
function printModelPropertiesBlock(
path: FastPath<Node & { properties?: (ModelPropertyNode | ModelSpreadPropertyNode)[] }>,
options: ADLPrettierOptions,
print: PrettierChildPrint
) {
const node = path.getNode();
if (!node?.properties || node.properties.length === 0) {
return "{}";
}
const seperator = isModelAValue(path) ? "," : ";";
return concat([
"{",
indent(
concat([
hardline,
join(
hardline,
path.map((x) => concat([print(x as any), seperator]), "properties")
),
])
),
hardline,
"}",
]);
}
/**
* Figure out if this model is being used as a definition or value.
* @returns true if the model is used as a value(e.g. decorator value), false if it is used as a model definition.
*/
function isModelAValue(path: FastPath<Node>): boolean {
let count = 0;
let node: Node | null = path.getValue();
do {
switch (node.kind) {
case SyntaxKind.ModelStatement:
case SyntaxKind.AliasStatement:
return false;
case SyntaxKind.DecoratorExpression:
return true;
}
} while ((node = path.getParentNode(count++)));
return true;
}
export function printModelProperty(
path: FastPath<ModelPropertyNode>,
options: ADLPrettierOptions,
print: PrettierChildPrint
) {
const node = path.getValue();
return concat([
printDecorators(path as FastPath<DecorableNode>, options, print, { tryInline: true }),
path.call(print, "id"),
node.optional ? "?: " : ": ",
path.call(print, "value"),
]);
}
function isModelExpressionInBlock(path: FastPath<ModelExpressionNode>) {
const parent: Node | null = path.getParentNode() as any;
switch (parent?.kind) {
case SyntaxKind.OperationStatement:
return false;
default:
return true;
}
}
export function printNamespaceStatement(
path: FastPath<NamespaceStatementNode>,
options: ADLPrettierOptions,
print: PrettierChildPrint
) {
const printNamespace = (
path: FastPath<NamespaceStatementNode>,
names: Doc[],
suffix: Doc | string
) => {};
const printNested = (currentPath: FastPath<NamespaceStatementNode>, parentNames: Doc[]): Doc => {
const names = [...parentNames, currentPath.call(print, "name")];
const currentNode = currentPath.getNode();
if (
!Array.isArray(currentNode?.statements) &&
currentNode?.statements?.kind === SyntaxKind.NamespaceStatement
) {
return path.call((x) => printNested(x, names), "statements");
}
const suffix =
currentNode?.statements === undefined
? ";"
: concat([
" {",
indent(concat([hardline, printStatementSequence(path, options, print, "statements")])),
hardline,
"}",
]);
return concat([
printDecorators(path, options, print, { tryInline: false }),
`namespace `,
join(".", names),
suffix,
]);
};
return printNested(path, []);
}
export function printOperationStatement(
path: FastPath<OperationStatementNode>,
options: ADLPrettierOptions,
print: PrettierChildPrint
) {
return concat([
printDecorators(path as FastPath<DecorableNode>, options, print, { tryInline: true }),
`op `,
path.call(print, "id"),
"(",
path.call(print, "parameters"),
"): ",
path.call(print, "returnType"),
`;`,
]);
}
export function printStatementSequence<T extends Node>(
path: FastPath<T>,
options: ADLPrettierOptions,
print: PrettierChildPrint,
property: keyof T
) {
const node = path.getValue();
const parts: Doc[] = [];
const lastStatement = getLastStatement((node[property] as any) as Statement[]);
path.each((statementPath) => {
const node = path.getValue();
if (node.kind === SyntaxKind.EmptyStatement) {
return;
}
const printed = print(statementPath);
parts.push(printed);
if (node !== lastStatement) {
parts.push(hardline);
if (isNextLineEmpty(options.originalText, node, options.locEnd)) {
parts.push(hardline);
}
}
}, property);
return concat(parts);
}
function getLastStatement(statements: Statement[]): Statement | undefined {
for (let i = statements.length - 1; i >= 0; i--) {
const statement = statements[i];
if (statement.kind !== SyntaxKind.EmptyStatement) {
return statement;
}
}
return undefined;
}
export function printUnion(
path: FastPath<UnionExpressionNode>,
options: object,
print: PrettierChildPrint
) {
const node = path.getValue();
const shouldHug = shouldHugType(node);
const types = path.map((typePath) => {
let printedType: string | prettier.Doc = print(typePath);
if (!shouldHug) {
printedType = align(2, printedType);
}
return printedType;
}, "options");
if (shouldHug) {
return join(" | ", types);
}
const shouldAddStartLine = true;
const code = [
ifBreak(concat([shouldAddStartLine ? line : "", "| "]), ""),
join(concat([line, "| "]), types),
];
return group(indent(concat(code)));
}
function shouldHugType(node: Node) {
if (node.kind === SyntaxKind.UnionExpression || node.kind === SyntaxKind.IntersectionExpression) {
return node.options.length < 4;
}
return false;
}
export function printTypeReference(
path: prettier.FastPath<TypeReferenceNode>,
options: ADLPrettierOptions,
print: PrettierChildPrint
): prettier.doc.builders.Doc {
const type = path.call(print, "target");
const template = printTemplateParameters(path, options, print, "arguments");
return concat([type, template]);
}
export function printStringLiteral(
path: prettier.FastPath<StringLiteralNode>,
options: ADLPrettierOptions
): prettier.doc.builders.Doc {
const node = path.getValue();
return getRawText(node, options);
}
export function printNumberLiteral(
path: prettier.FastPath<NumericLiteralNode>,
options: ADLPrettierOptions
): prettier.doc.builders.Doc {
const node = path.getValue();
return getRawText(node, options);
}
/**
* @param node Node that has postition information.
* @param options Prettier options
* @returns Raw text in the file for the given node.
*/
function getRawText(node: TextRange, options: ADLPrettierOptions) {
return options.originalText.slice(node.pos, node.end);
}

Просмотреть файл

@ -0,0 +1,10 @@
import { Doc, FastPath, ParserOptions } from "prettier";
import { DecoratorExpressionNode, Node } from "../../compiler/types.js";
export interface ADLPrettierOptions extends ParserOptions {}
export type PrettierChildPrint = (path: FastPath<Node>, index?: number) => Doc;
export interface DecorableNode {
decorators: DecoratorExpressionNode[];
}

Просмотреть файл

@ -1,15 +1,13 @@
import { throwDiagnostic } from "../compiler/diagnostics.js";
import { Program } from "../compiler/program.js";
import { ModelTypeProperty, NamespaceType, Type } from "../compiler/types.js";
const docs = new Map<Type, string>();
const docsKey = Symbol();
export function doc(program: Program, target: Type, text: string) {
docs.set(target, text);
program.stateMap(docsKey).set(target, text);
}
export function getDoc(target: Type) {
return docs.get(target);
export function getDoc(program: Program, target: Type): string {
return program.stateMap(docsKey).get(target);
}
export function inspectType(program: Program, target: Type, text: string) {
@ -22,26 +20,33 @@ export function inspectTypeName(program: Program, target: Type, text: string) {
console.log(program.checker!.getTypeName(target));
}
const intrinsics = new Set<Type>();
const intrinsicsKey = Symbol();
export function intrinsic(program: Program, target: Type) {
intrinsics.add(target);
program.stateSet(intrinsicsKey).add(target);
}
export function isIntrinsic(target: Type) {
return intrinsics.has(target);
export function isIntrinsic(program: Program, target: Type | undefined) {
if (!target) {
return false;
}
return program.stateSet(intrinsicsKey).has(target);
}
// Walks the assignmentType chain to find the core intrinsic type, if any
export function getIntrinsicType(target: Type | undefined): string | undefined {
export function getIntrinsicType(program: Program, target: Type | undefined): string | undefined {
while (target) {
if (target.kind === "Model") {
if (isIntrinsic(target)) {
if (isIntrinsic(program, target)) {
return target.name;
}
target = (target.assignmentType?.kind === "Model" && target.assignmentType) || undefined;
if (target.baseModels.length === 1) {
target = target.baseModels[0];
} else {
target = undefined;
}
} else if (target.kind === "ModelProperty") {
return getIntrinsicType(target.type);
return getIntrinsicType(program, target.type);
} else {
break;
}
@ -50,185 +55,194 @@ export function getIntrinsicType(target: Type | undefined): string | undefined {
return undefined;
}
const numericTypes = new Set<string>();
const numericTypesKey = Symbol();
export function numeric(program: Program, target: Type) {
if (!isIntrinsic(target)) {
throwDiagnostic("Cannot apply @numeric decorator to non-intrinsic type.", target);
if (!isIntrinsic(program, target)) {
program.reportDiagnostic("Cannot apply @numeric decorator to non-intrinsic type.", target);
return;
}
if (target.kind === "Model") {
numericTypes.add(target.name);
} else {
throwDiagnostic("Cannot apply @numeric decorator to non-model type.", target);
if (target.kind !== "Model") {
program.reportDiagnostic("Cannot apply @numeric decorator to non-model type.", target);
return;
}
program.stateSet(numericTypesKey).add(target.name);
}
export function isNumericType(target: Type): boolean {
const intrinsicType = getIntrinsicType(target);
return intrinsicType !== undefined && numericTypes.has(intrinsicType);
export function isNumericType(program: Program, target: Type): boolean {
const intrinsicType = getIntrinsicType(program, target);
return intrinsicType !== undefined && program.stateSet(numericTypesKey).has(intrinsicType);
}
// -- @format decorator ---------------------
const formatValues = new Map<Type, string>();
const formatValuesKey = Symbol();
export function format(program: Program, target: Type, format: string) {
if (target.kind === "Model" || target.kind === "ModelProperty") {
// Is it a model type that ultimately derives from 'string'?
if (getIntrinsicType(target) === "string") {
formatValues.set(target, format);
} else {
throwDiagnostic("Cannot apply @format to a non-string type", target);
}
} else {
throwDiagnostic("Cannot apply @format to anything that isn't a Model or ModelProperty", target);
if (target.kind !== "Model" && target.kind !== "ModelProperty") {
program.reportDiagnostic(
"Cannot apply @format to anything that isn't a Model or ModelProperty",
target
);
return;
}
if (getIntrinsicType(program, target) !== "string") {
program.reportDiagnostic("Cannot apply @format to a non-string type", target);
return;
}
program.stateMap(formatValuesKey).set(target, format);
}
export function getFormat(target: Type): string | undefined {
return formatValues.get(target);
export function getFormat(program: Program, target: Type): string | undefined {
return program.stateMap(formatValuesKey).get(target);
}
// -- @minLength decorator ---------------------
const minLengthValues = new Map<Type, number>();
const minLengthValuesKey = Symbol();
export function minLength(program: Program, target: Type, minLength: number) {
if (target.kind === "Model" || target.kind === "ModelProperty") {
// Is it a model type that ultimately derives from 'string'?
if (getIntrinsicType(target) === "string") {
minLengthValues.set(target, minLength);
} else {
throwDiagnostic("Cannot apply @minLength to a non-string type", target);
}
} else {
throwDiagnostic(
if (target.kind !== "Model" && target.kind !== "ModelProperty") {
program.reportDiagnostic(
"Cannot apply @minLength to anything that isn't a Model or ModelProperty",
target
);
return;
}
if (getIntrinsicType(program, target) !== "string") {
program.reportDiagnostic("Cannot apply @minLength to a non-string type", target);
return;
}
program.stateMap(minLengthValuesKey).set(target, minLength);
}
export function getMinLength(target: Type): number | undefined {
return minLengthValues.get(target);
export function getMinLength(program: Program, target: Type): number | undefined {
return program.stateMap(minLengthValuesKey).get(target);
}
// -- @maxLength decorator ---------------------
const maxLengthValues = new Map<Type, number>();
const maxLengthValuesKey = Symbol();
export function maxLength(program: Program, target: Type, maxLength: number) {
if (target.kind === "Model" || target.kind === "ModelProperty") {
// Is it a model type that ultimately derives from 'string'?
if (getIntrinsicType(target) === "string") {
maxLengthValues.set(target, maxLength);
} else {
throwDiagnostic("Cannot apply @maxLength to a non-string type", target);
}
} else {
throwDiagnostic(
if (target.kind !== "Model" && target.kind !== "ModelProperty") {
program.reportDiagnostic(
"Cannot apply @maxLength to anything that isn't a Model or ModelProperty",
target
);
return;
}
if (getIntrinsicType(program, target) !== "string") {
program.reportDiagnostic("Cannot apply @maxLength to a non-string type", target);
return;
}
program.stateMap(maxLengthValuesKey).set(target, maxLength);
}
export function getMaxLength(target: Type): number | undefined {
return maxLengthValues.get(target);
export function getMaxLength(program: Program, target: Type): number | undefined {
return program.stateMap(maxLengthValuesKey).get(target);
}
// -- @minValue decorator ---------------------
const minValues = new Map<Type, number>();
const minValuesKey = Symbol();
export function minValue(program: Program, target: Type, minValue: number) {
if (target.kind === "Model" || target.kind === "ModelProperty") {
// Is it ultimately a numeric type?
if (isNumericType(target)) {
minValues.set(target, minValue);
} else {
throwDiagnostic("Cannot apply @minValue to a non-numeric type", target);
}
} else {
throwDiagnostic(
if (target.kind !== "Model" && target.kind !== "ModelProperty") {
program.reportDiagnostic(
"Cannot apply @minValue to anything that isn't a Model or ModelProperty",
target
);
}
if (!isNumericType(program, target)) {
program.reportDiagnostic("Cannot apply @minValue to a non-numeric type", target);
return;
}
program.stateMap(minValuesKey).set(target, minValue);
}
export function getMinValue(target: Type): number | undefined {
return minValues.get(target);
export function getMinValue(program: Program, target: Type): number | undefined {
return program.stateMap(minValuesKey).get(target);
}
// -- @maxValue decorator ---------------------
const maxValues = new Map<Type, number>();
const maxValuesKey = Symbol();
export function maxValue(program: Program, target: Type, maxValue: number) {
if (target.kind === "Model" || target.kind === "ModelProperty") {
// Is it ultimately a numeric type?
if (isNumericType(target)) {
maxValues.set(target, maxValue);
} else {
throwDiagnostic("Cannot apply @maxValue to a non-numeric type", target);
}
} else {
throwDiagnostic(
if (target.kind !== "Model" && target.kind !== "ModelProperty") {
program.reportDiagnostic(
"Cannot apply @maxValue to anything that isn't a Model or ModelProperty",
target
);
return;
}
if (!isNumericType(program, target)) {
program.reportDiagnostic("Cannot apply @maxValue to a non-numeric type", target);
return;
}
program.stateMap(maxValuesKey).set(target, maxValue);
}
export function getMaxValue(target: Type): number | undefined {
return maxValues.get(target);
export function getMaxValue(program: Program, target: Type): number | undefined {
return program.stateMap(maxValuesKey).get(target);
}
// -- @secret decorator ---------------------
const secretTypes = new Map<Type, boolean>();
const secretTypesKey = Symbol();
export function secret(program: Program, target: Type) {
if (target.kind === "Model") {
// Is it a model type that ultimately derives from 'string'?
if (getIntrinsicType(target) === "string") {
secretTypes.set(target, true);
} else {
throwDiagnostic("Cannot apply @secret to a non-string type", target);
}
} else {
throwDiagnostic("Cannot apply @secret to anything that isn't a Model", target);
if (target.kind !== "Model") {
program.reportDiagnostic("Cannot apply @secret to anything that isn't a Model", target);
return;
}
if (getIntrinsicType(program, target) !== "string") {
program.reportDiagnostic("Cannot apply @secret to a non-string type", target);
return;
}
program.stateMap(secretTypesKey).set(target, true);
}
export function isSecret(target: Type): boolean | undefined {
return secretTypes.get(target);
export function isSecret(program: Program, target: Type): boolean | undefined {
return program.stateMap(secretTypesKey).get(target);
}
// -- @visibility decorator ---------------------
const visibilitySettings = new Map<Type, string[]>();
const visibilitySettingsKey = Symbol();
export function visibility(program: Program, target: Type, ...visibilities: string[]) {
if (target.kind === "ModelProperty") {
visibilitySettings.set(target, visibilities);
} else {
throwDiagnostic("The @visibility decorator can only be applied to model properties.", target);
if (target.kind !== "ModelProperty") {
program.reportDiagnostic(
"The @visibility decorator can only be applied to model properties.",
target
);
return;
}
program.stateMap(visibilitySettingsKey).set(target, visibilities);
}
export function getVisibility(target: Type): string[] | undefined {
return visibilitySettings.get(target);
export function getVisibility(program: Program, target: Type): string[] | undefined {
return program.stateMap(visibilitySettingsKey).get(target);
}
export function withVisibility(program: Program, target: Type, ...visibilities: string[]) {
if (target.kind !== "Model") {
throwDiagnostic("The @withVisibility decorator can only be applied to models.", target);
program.reportDiagnostic(
"The @withVisibility decorator can only be applied to models.",
target
);
return;
}
const filter = (_: any, prop: ModelTypeProperty) => {
const vis = getVisibility(prop);
const vis = getVisibility(program, prop);
return vis !== undefined && visibilities.filter((v) => !vis.includes(v)).length > 0;
};
@ -248,56 +262,63 @@ function mapFilterOut(
// -- @list decorator ---------------------
const listProperties = new Set<Type>();
const listPropertiesKey = Symbol();
export function list(program: Program, target: Type) {
if (target.kind === "Operation" || target.kind === "ModelProperty") {
listProperties.add(target);
} else {
throwDiagnostic(
"The @list decorator can only be applied to interface or model properties.",
if (target.kind !== "Operation" && target.kind !== "ModelProperty") {
program.reportDiagnostic(
"The @list decorator can only be applied to operations or model properties.",
target
);
return;
}
program.stateSet(listPropertiesKey).add(target);
}
export function isList(target: Type): boolean {
return listProperties.has(target);
export function isList(program: Program, target: Type): boolean {
return program.stateSet(listPropertiesKey).has(target);
}
// -- @tag decorator ---------------------
const tagProperties = new Map<Type, string[]>();
const tagPropertiesKey = Symbol();
// Set a tag on an operation or namespace. There can be multiple tags on either an
// operation or namespace.
export function tag(program: Program, target: Type, tag: string) {
if (target.kind === "Operation" || target.kind === "Namespace") {
const tags = tagProperties.get(target);
if (tags) {
tags.push(tag);
} else {
tagProperties.set(target, [tag]);
}
if (target.kind !== "Operation" && target.kind !== "Namespace") {
program.reportDiagnostic(
"The @tag decorator can only be applied to namespaces or operations.",
target
);
return;
}
const tags = program.stateMap(tagPropertiesKey).get(target);
if (tags) {
tags.push(tag);
} else {
throwDiagnostic("The @tag decorator can only be applied to namespace or operation.", target);
program.stateMap(tagPropertiesKey).set(target, [tag]);
}
}
// Return the tags set on an operation or namespace
export function getTags(target: Type): string[] {
return tagProperties.get(target) || [];
export function getTags(program: Program, target: Type): string[] {
return program.stateMap(tagPropertiesKey).get(target) || [];
}
// Merge the tags for a operation with the tags that are on the namespace it resides within.
//
// TODO: (JC) We'll need to update this for nested namespaces
export function getAllTags(namespace: NamespaceType, target: Type): string[] | undefined {
export function getAllTags(
program: Program,
namespace: NamespaceType,
target: Type
): string[] | undefined {
const tags = new Set<string>();
for (const t of getTags(namespace)) {
for (const t of getTags(program, namespace)) {
tags.add(t);
}
for (const t of getTags(target)) {
for (const t of getTags(program, target)) {
tags.add(t);
}
return tags.size > 0 ? Array.from(tags) : undefined;

Просмотреть файл

@ -0,0 +1,2 @@
import "../dist/lib/decorators.js";
import "./lib.adl";

Просмотреть файл

@ -1,6 +1,6 @@
{
"name": "@azure-tools/adl",
"version": "0.9.0",
"version": "0.11.0",
"description": "ADL Compiler Preview",
"author": "Microsoft Corporation",
"license": "MIT",
@ -38,29 +38,36 @@
"watch": "tsc -p . --watch",
"dogfood": "node scripts/dogfood.js",
"test": "mocha --timeout 5000 --require source-map-support/register --ignore 'dist/test/manual/**/*.js' 'dist/test/**/*.js'",
"test-official": "mocha --forbid-only --timeout 5000 --require source-map-support/register --ignore 'dist/test/manual/**/*.js' 'dist/test/**/*.js'",
"regen-samples": "node scripts/regen-samples.js",
"regen-nonascii-maps": "node scripts/regen-nonascii-maps.js",
"fuzz": "node dist/test/manual/fuzz.js"
"regen-nonascii": "node scripts/regen-nonascii.js",
"fuzz": "node dist/test/manual/fuzz.js run"
},
"dependencies": {
"autorest": "~3.0.6335",
"ajv": "~8.4.0",
"glob": "~7.1.6",
"js-yaml": "~4.1.0",
"mkdirp": "~1.0.4",
"prettier": "~2.2.1",
"resolve": "~1.20.0",
"vscode-languageserver": "~7.0.0",
"vscode-languageserver-textdocument": "~1.0.1",
"vscode-languageserver": "~7.0.0",
"yargs": "~16.2.0"
},
"devDependencies": {
"@types/glob": "~7.1.3",
"@types/js-yaml": "~4.0.1",
"@types/mkdirp": "~1.0.1",
"@types/mocha": "~7.0.2",
"@types/node": "~14.0.27",
"@types/prettier": "^2.0.2",
"@types/resolve": "~1.20.0",
"@types/yargs": "~15.0.12",
"grammarkdown": "~3.1.2",
"mocha": "~8.3.2",
"prettier": "~2.2.1",
"prettier-plugin-organize-imports": "~1.1.1",
"source-map-support": "~0.5.19",
"typescript": "~4.2.4"
"typescript": "~4.3.2"
}
}

Просмотреть файл

@ -9,8 +9,11 @@ import { fileURLToPath } from "url";
const MIN_NONASCII_CODEPOINT = 0x80;
const MAX_UNICODE_CODEPOINT = 0x10ffff;
const isStartRegex = /[\p{ID_Start}\u{2118}\u{212E}\u{309B}\u{309C}]/u;
const isContinueRegex = /[\p{ID_Continue}\u{00B7}\u{0387}\u{19DA}\u{1369}\u{136A}\u{136B}\u{136C}\u{136D}\u{136E}\u{136F}\u{1370}\u{1371}]/u;
// Includes Other_ID_Start
const isStartRegex = /[\p{ID_Start}]/u;
// Includes Other_ID_Start and Other_ID_Continue
const isContinueRegex = /[\p{ID_Continue}\u{200c}\u{200d}]/u;
function isStart(c) {
return isStartRegex.test(c);
@ -50,15 +53,19 @@ const src = `//
//
// Based on:
// - http://www.unicode.org/reports/tr31/
// - https://www.ecma-international.org/ecma-262/6.0/#sec-names-and-keywords
// - https://www.ecma-international.org/ecma-262/11.0/#sec-names-and-keywords
//
// ADL's identifier naming rules are currently the same as JavaScript's.
//
/**
* @internal
*
* Map of non-ascii characters that are valid at the start of an identifier.
* Each pair of numbers represents an inclusive range of code points.
*
* Corresponds to code points outside the ASCII range with property ID_Start or
* Other_ID_Start.
*/
// prettier-ignore
export const nonAsciiIdentifierStartMap: readonly number[] = [
@ -68,8 +75,12 @@ ${formatPairs(startMap)}
/**
* @internal
*
* Map of non-ascii chacters that are valid after the first character in and identifier.
* Each pair of numbers represents an inclusive range of code points.
* Map of non-ascii chacters that are valid after the first character in and
* identifier. Each pair of numbers represents an inclusive range of code
* points.
*
* Corresponds to code points outside the ASCII range with property ID_Continue,
* Other_ID_Start, or Other_ID_Continue, plus ZWNJ and ZWJ.
*/
//prettier-ignore
export const nonAsciiIdentifierContinueMap: readonly number[] = [
@ -77,5 +88,5 @@ ${formatPairs(continueMap)}
];
`;
const file = resolve(fileURLToPath(import.meta.url), "../../compiler/non-ascii-maps.ts");
const file = resolve(fileURLToPath(import.meta.url), "../../compiler/nonascii.ts");
writeFileSync(file, src);

Просмотреть файл

@ -1,3 +1,4 @@
import { fileURLToPath } from "url";
import { TextDocument } from "vscode-languageserver-textdocument";
import {
Connection,
@ -21,7 +22,8 @@ let documents: TextDocuments<TextDocument>;
main();
function main() {
log(`** ADL Language Server v${adlVersion} **`);
log(`ADL language server v${adlVersion}\n`);
log(`Module: ${fileURLToPath(import.meta.url)}`);
log(`Command Line: ${JSON.stringify(process.argv)}`);
connection = createConnection(ProposedFeatures.all);
@ -48,11 +50,11 @@ function checkChange(change: TextDocumentChangeEvent<TextDocument>) {
const diagnostics: Diagnostic[] = [];
for (const each of parseDiagnostics) {
const start = document.positionAt(each.pos);
const end = document.positionAt(each.end);
const start = document.positionAt(each.pos ?? 0);
const end = document.positionAt(each.end ?? 0);
const range = Range.create(start, end);
const severity = convertSeverity(each.severity);
const diagnostic = Diagnostic.create(range, each.message, severity, "ADL");
const diagnostic = Diagnostic.create(range, each.message, severity, each.code, "ADL");
diagnostics.push(diagnostic);
}

Просмотреть файл

@ -0,0 +1,165 @@
import { ok, strictEqual } from "assert";
import { ModelType, UnionType } from "../../compiler/types.js";
import { createTestHost, TestHost } from "../test-host.js";
describe("adl: aliases", () => {
let testHost: TestHost;
beforeEach(async () => {
testHost = await createTestHost();
});
it("can alias a union expression", async () => {
testHost.addAdlFile(
"main.adl",
`
alias Foo = int32 | string;
alias Bar = "hi" | 10;
alias FooBar = Foo | Bar;
@test model A {
prop: FooBar
}
`
);
const { A } = (await testHost.compile("./")) as {
A: ModelType;
};
const propType: UnionType = A.properties.get("prop")!.type as UnionType;
strictEqual(propType.kind, "Union");
strictEqual(propType.options.length, 4);
strictEqual(propType.options[0].kind, "Model");
strictEqual(propType.options[1].kind, "Model");
strictEqual(propType.options[2].kind, "String");
strictEqual(propType.options[3].kind, "Number");
});
it("can alias a deep union expression", async () => {
testHost.addAdlFile(
"main.adl",
`
alias Foo = int32 | string;
alias Bar = "hi" | 10;
alias Baz = Foo | Bar;
alias FooBar = Baz | "bye";
@test model A {
prop: FooBar
}
`
);
const { A } = (await testHost.compile("./")) as {
A: ModelType;
};
const propType: UnionType = A.properties.get("prop")!.type as UnionType;
strictEqual(propType.kind, "Union");
strictEqual(propType.options.length, 5);
strictEqual(propType.options[0].kind, "Model");
strictEqual(propType.options[1].kind, "Model");
strictEqual(propType.options[2].kind, "String");
strictEqual(propType.options[3].kind, "Number");
strictEqual(propType.options[4].kind, "String");
});
it("can alias a union expression with parameters", async () => {
testHost.addAdlFile(
"main.adl",
`
alias Foo<T> = int32 | T;
@test model A {
prop: Foo<"hi">
}
`
);
const { A } = (await testHost.compile("./")) as {
A: ModelType;
};
const propType: UnionType = A.properties.get("prop")!.type as UnionType;
strictEqual(propType.kind, "Union");
strictEqual(propType.options.length, 2);
strictEqual(propType.options[0].kind, "Model");
strictEqual(propType.options[1].kind, "String");
});
it("can alias a deep union expression with parameters", async () => {
testHost.addAdlFile(
"main.adl",
`
alias Foo<T> = int32 | T;
alias Bar<T, U> = Foo<T> | Foo<U>;
@test model A {
prop: Bar<"hi", 42>
}
`
);
const { A } = (await testHost.compile("./")) as {
A: ModelType;
};
const propType: UnionType = A.properties.get("prop")!.type as UnionType;
strictEqual(propType.kind, "Union");
strictEqual(propType.options.length, 4);
strictEqual(propType.options[0].kind, "Model");
strictEqual(propType.options[1].kind, "String");
strictEqual(propType.options[2].kind, "Model");
strictEqual(propType.options[3].kind, "Number");
});
it("can alias an intersection expression", async () => {
testHost.addAdlFile(
"main.adl",
`
alias Foo = {a: string} & {b: string};
alias Bar = {c: string} & {d: string};
alias FooBar = Foo & Bar;
@test model A {
prop: FooBar
}
`
);
const { A } = (await testHost.compile("./")) as {
A: ModelType;
};
const propType: ModelType = A.properties.get("prop")!.type as ModelType;
strictEqual(propType.kind, "Model");
strictEqual(propType.properties.size, 4);
ok(propType.properties.has("a"));
ok(propType.properties.has("b"));
ok(propType.properties.has("c"));
ok(propType.properties.has("d"));
});
it("can be used like any model", async () => {
testHost.addAdlFile(
"main.adl",
`
@test model Test { a: string };
alias Alias = Test;
@test model A extends Alias { };
@test model B { ... Alias };
@test model C { c: Alias };
`
);
const { Test, A, B, C } = (await testHost.compile("./")) as {
Test: ModelType;
A: ModelType;
B: ModelType;
C: ModelType;
};
strictEqual(A.baseModels[0], Test);
ok(B.properties.has("a"));
strictEqual(C.properties.get("c")!.type, Test);
});
});

Просмотреть файл

@ -0,0 +1,27 @@
import { match, strictEqual } from "assert";
import { createTestHost, TestHost } from "../test-host.js";
describe("adl: semantic checks on source with parse errors", () => {
let testHost: TestHost;
beforeEach(async () => {
testHost = await createTestHost();
});
it("reports semantic errors in addition to parse errors", async () => {
testHost.addAdlFile(
"main.adl",
`model M extends Q {
a: B;
a: C;
`
);
const diagnostics = await testHost.diagnose("/");
strictEqual(diagnostics.length, 4);
match(diagnostics[0].message, /Property expected/);
match(diagnostics[1].message, /Unknown identifier Q/);
match(diagnostics[2].message, /Unknown identifier B/);
match(diagnostics[3].message, /Unknown identifier C/);
});
});

Просмотреть файл

@ -1,39 +1,42 @@
import { rejects } from "assert";
import { match, strictEqual } from "assert";
import { Diagnostic } from "../../compiler/types.js";
import { createTestHost, TestHost } from "../test-host.js";
describe("duplicate declarations", () => {
describe("adl: duplicate declarations", () => {
let testHost: TestHost;
beforeEach(async () => {
testHost = await createTestHost();
});
it("throws for duplicate template parameters", async () => {
it("reports duplicate template parameters", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
model A<T, T> { }
`
);
await rejects(testHost.compile("/"));
const diagnostics = await testHost.diagnose("/");
assertDuplicates(diagnostics);
});
it("throws for duplicate model declarations in global scope", async () => {
it("reports duplicate model declarations in global scope", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
model A { }
model A { }
`
);
await rejects(testHost.compile("/"));
const diagnostics = await testHost.diagnose("/");
assertDuplicates(diagnostics);
});
it("throws for duplicate model declarations in a single namespace", async () => {
it("reports duplicate model declarations in a single namespace", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
namespace Foo;
model A { }
@ -41,12 +44,13 @@ describe("duplicate declarations", () => {
`
);
await rejects(testHost.compile("/"));
const diagnostics = await testHost.diagnose("/");
assertDuplicates(diagnostics);
});
it("throws for duplicate model declarations in across multiple namespaces", async () => {
it("reports duplicate model declarations across multiple namespaces", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
namespace N {
model A { };
@ -58,10 +62,18 @@ describe("duplicate declarations", () => {
`
);
await rejects(testHost.compile("/"));
const diagnostics = await testHost.diagnose("/");
assertDuplicates(diagnostics);
});
it("throws for duplicate model declarations in across multiple files and namespaces", async () => {
it("reports duplicate model declarations across multiple files and namespaces", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -79,6 +91,14 @@ describe("duplicate declarations", () => {
`
);
await rejects(testHost.compile("/"));
const diagnostics = await testHost.diagnose("/");
assertDuplicates(diagnostics);
});
});
function assertDuplicates(diagnostics: readonly Diagnostic[]) {
strictEqual(diagnostics.length, 2);
for (const diagnostic of diagnostics) {
match(diagnostic.message, /Duplicate name/);
}
}

Просмотреть файл

@ -0,0 +1,76 @@
import { ok, strictEqual } from "assert";
import { EnumMemberType, EnumType, ModelType } from "../../compiler/types.js";
import { createTestHost, TestHost } from "../test-host.js";
describe("adl: enums", () => {
let testHost: TestHost;
beforeEach(async () => {
testHost = await createTestHost();
});
it("can be valueless", async () => {
testHost.addAdlFile(
"main.adl",
`
@test enum E {
A, B, C
}
`
);
const { E } = (await testHost.compile("./")) as {
E: EnumType;
};
ok(E);
ok(!E.members[0].value);
ok(!E.members[1].value);
ok(!E.members[2].value);
});
it("can have values", async () => {
testHost.addAdlFile(
"main.adl",
`
@test enum E {
@test("A") A: "a";
@test("B") B: "b";
@test("C") C: "c";
}
`
);
const { E, A, B, C } = (await testHost.compile("./")) as {
E: EnumType;
A: EnumMemberType;
B: EnumMemberType;
C: EnumMemberType;
};
ok(E);
strictEqual(A.value, "a");
strictEqual(B.value, "b");
strictEqual(C.value, "c");
});
it("can be a model property", async () => {
testHost.addAdlFile(
"main.adl",
`
namespace Foo;
enum E { A, B, C }
@test model Foo {
prop: E;
}
`
);
const { Foo } = (await testHost.compile("./")) as {
Foo: ModelType;
};
ok(Foo);
strictEqual(Foo.properties.get("prop")!.type.kind, "Enum");
});
});

Просмотреть файл

@ -0,0 +1,101 @@
import { assert } from "console";
import { createTestHost, TestHost } from "../test-host.js";
describe("adl: global namespace", () => {
let testHost: TestHost;
beforeEach(async () => {
testHost = await createTestHost();
});
describe("it adds top level entities to the global namespace", () => {
it("adds top-level namespaces", async () => {
testHost.addAdlFile("main.adl", `namespace Foo {}`);
await testHost.compile("./");
const globalNamespaceType = testHost.program.checker?.getGlobalNamespaceType();
assert(
globalNamespaceType?.namespaces.get("Foo"),
"Namespace Foo was added to global namespace type"
);
});
it("adds top-level models", async () => {
testHost.addAdlFile("main.adl", `model MyModel {}`);
await testHost.compile("./");
const globalNamespaceType = testHost.program.checker?.getGlobalNamespaceType();
assert(
globalNamespaceType?.models.get("MyModel"),
"model MyModel was added to global namespace type"
);
});
it("adds top-level oeprations", async () => {
testHost.addAdlFile("main.adl", `op myOperation(): string;`);
await testHost.compile("./");
const globalNamespaceType = testHost.program.checker?.getGlobalNamespaceType();
assert(
globalNamespaceType?.operations.get("myOperation"),
"operation myOperation was added to global namespace type"
);
});
});
describe("it adds top level entities used in other files to the global namespace", () => {
beforeEach(() => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
model Base {}
`
);
});
it("adds top-level namespaces", async () => {
testHost.addAdlFile("a.adl", `namespace Foo {}`);
await testHost.compile("./");
const globalNamespaceType = testHost.program.checker?.getGlobalNamespaceType();
assert(
globalNamespaceType?.namespaces.get("Foo"),
"Namespace Foo was added to global namespace type"
);
assert(
globalNamespaceType?.namespaces.get("Base"),
"Should still reference main file top-level entities"
);
});
it("adds top-level models", async () => {
testHost.addAdlFile("a.adl", `model MyModel {}`);
await testHost.compile("./");
const globalNamespaceType = testHost.program.checker?.getGlobalNamespaceType();
assert(
globalNamespaceType?.models.get("MyModel"),
"model MyModel was added to global namespace type"
);
});
it("adds top-level oeprations", async () => {
testHost.addAdlFile("a.adl", `op myOperation(): string;`);
await testHost.compile("./");
const globalNamespaceType = testHost.program.checker?.getGlobalNamespaceType();
assert(
globalNamespaceType?.operations.get("myOperation"),
"operation myOperation was added to global namespace type"
);
});
});
});

Просмотреть файл

@ -1,6 +1,6 @@
import { createTestHost, TestHost } from "../test-host.js";
describe("loader", () => {
describe("adl: loader", () => {
let testHost: TestHost;
beforeEach(async () => {
@ -10,7 +10,7 @@ describe("loader", () => {
it("loads ADL and JS files", async () => {
testHost.addJsFile("blue.js", { blue() {} });
testHost.addAdlFile(
"a.adl",
"main.adl",
`
import "./b.adl";
import "./blue.js";
@ -39,6 +39,6 @@ describe("loader", () => {
`
);
await testHost.compile("a.adl");
await testHost.compile("main.adl");
});
});

Просмотреть файл

@ -2,7 +2,7 @@ import { ok, strictEqual } from "assert";
import { ModelType, NamespaceType, Type } from "../../compiler/types.js";
import { createTestHost, TestHost } from "../test-host.js";
describe("namespaces with blocks", () => {
describe("adl: namespaces with blocks", () => {
const blues = new WeakSet();
function blue(_: any, target: Type) {
blues.add(target);
@ -17,8 +17,9 @@ describe("namespaces with blocks", () => {
it("can be decorated", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
import "./blue.js";
@blue @test namespace Z.Q;
@blue @test namespace N { }
@blue @test namespace X.Y { }
@ -37,7 +38,7 @@ describe("namespaces with blocks", () => {
it("merges like namespaces", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
@test
namespace N { @test model X { x: string } }
@ -58,6 +59,14 @@ describe("namespaces with blocks", () => {
});
it("merges like namespaces across files", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
import "./c.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -90,6 +99,14 @@ describe("namespaces with blocks", () => {
});
it("merges sub-namespaces across files", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
import "./c.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -117,7 +134,7 @@ describe("namespaces with blocks", () => {
it("can see things in outer scope same file", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
model A { }
namespace N { model B extends A { } }
@ -127,6 +144,14 @@ describe("namespaces with blocks", () => {
});
it("can see things in outer scope cross file", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
import "./c.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -150,9 +175,30 @@ describe("namespaces with blocks", () => {
);
await testHost.compile("./");
});
it("accumulates declarations inside of it", async () => {
testHost.addAdlFile(
"main.adl",
`
@test namespace Foo {
namespace Bar { };
op Baz(): {};
model Qux { };
}
`
);
const { Foo } = (await testHost.compile("./")) as {
Foo: NamespaceType;
};
strictEqual(Foo.operations.size, 1);
strictEqual(Foo.models.size, 1);
strictEqual(Foo.namespaces.size, 1);
});
});
describe("blockless namespaces", () => {
describe("adl: blockless namespaces", () => {
const blues = new WeakSet();
function blue(_: any, target: Type) {
blues.add(target);
@ -166,6 +212,14 @@ describe("blockless namespaces", () => {
});
it("merges properly with other namespaces", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
import "./c.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -194,7 +248,7 @@ describe("blockless namespaces", () => {
it("does lookup correctly", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
namespace Repro;
model Yo {
@ -214,7 +268,7 @@ describe("blockless namespaces", () => {
it("does lookup correctly with nested namespaces", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
namespace Repro;
model Yo {
@ -244,7 +298,7 @@ describe("blockless namespaces", () => {
it("binds correctly", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
namespace N.M;
model A { }
@ -266,7 +320,7 @@ describe("blockless namespaces", () => {
it("works with blockful namespaces", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
@test
namespace N;
@ -293,6 +347,13 @@ describe("blockless namespaces", () => {
});
it("works with nested blockless and blockfull namespaces", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -326,7 +387,7 @@ describe("blockless namespaces", () => {
"a.adl",
`
import "./b.adl";
model M = N.X;
model M {x: N.X }
`
);
testHost.addAdlFile(
@ -339,4 +400,53 @@ describe("blockless namespaces", () => {
await testHost.compile("/a.adl");
});
it("accumulates declarations inside of it", async () => {
testHost.addAdlFile(
"a.adl",
`
@test namespace Foo;
namespace Bar { };
op Baz(): {};
model Qux { };
`
);
const { Foo } = (await testHost.compile("/a.adl")) as {
Foo: NamespaceType;
};
strictEqual(Foo.operations.size, 1);
strictEqual(Foo.models.size, 1);
strictEqual(Foo.namespaces.size, 1);
});
});
describe("adl: namespace type name", () => {
let testHost: TestHost;
beforeEach(async () => {
testHost = await createTestHost();
});
it("prefix with the namespace of the entity", async () => {
testHost.addAdlFile(
"a.adl",
`
namespace Foo;
@test()
model Model1 {}
namespace Other.Bar {
@test()
model Model2 {}
}
`
);
const { Model1, Model2 } = await testHost.compile("/a.adl");
strictEqual(testHost.program.checker?.getTypeName(Model1), "Foo.Model1");
strictEqual(testHost.program.checker?.getTypeName(Model2), "Foo.Other.Bar.Model2");
});
});

Просмотреть файл

@ -2,7 +2,7 @@ import { ok, strictEqual } from "assert";
import { ModelType, Type } from "../../compiler/types.js";
import { createTestHost, TestHost } from "../test-host.js";
describe("spread", () => {
describe("adl: spread", () => {
const blues = new WeakSet();
function blue(_: any, target: Type) {
blues.add(target);
@ -17,8 +17,9 @@ describe("spread", () => {
it("clones decorated properties", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
import "./blue.js";
model A { @blue foo: string }
model B { @blue bar: string }
@test model C { ... A, ... B }

Просмотреть файл

@ -2,7 +2,7 @@ import { rejects, strictEqual } from "assert";
import { ModelType } from "../../compiler/types";
import { createTestHost, TestHost } from "../test-host.js";
describe("using statements", () => {
describe("adl: using statements", () => {
let testHost: TestHost;
beforeEach(async () => {
@ -10,6 +10,13 @@ describe("using statements", () => {
});
it("works in global scope", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -33,6 +40,13 @@ describe("using statements", () => {
});
it("works in namespaces", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -57,6 +71,13 @@ describe("using statements", () => {
});
it("works with dotted namespaces", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -80,6 +101,13 @@ describe("using statements", () => {
});
it("throws errors for duplicate imported usings", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -101,6 +129,13 @@ describe("using statements", () => {
});
it("throws errors for different usings with the same bindings", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -126,6 +161,13 @@ describe("using statements", () => {
});
it("resolves 'local' decls over usings", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -151,6 +193,13 @@ describe("using statements", () => {
});
it("usings are local to a file", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -167,7 +216,7 @@ describe("using statements", () => {
}
namespace M {
model X = A;
model X { a: A };
}
`
);

Просмотреть файл

@ -0,0 +1,145 @@
import { deepStrictEqual } from "assert";
import { dirname, join, resolve } from "path";
import { fileURLToPath } from "url";
import { createSourceFile } from "../../compiler/diagnostics.js";
import { Diagnostic } from "../../compiler/types.js";
import { ConfigValidator } from "../../config/config-validator.js";
import { ADLRawConfig, loadADLConfigInDir } from "../../config/index.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
describe("adl: config file loading", () => {
describe("file discovery", async () => {
const scenarioRoot = resolve(__dirname, "../../../test/config/scenarios");
const loadTestConfig = async (folderName: string) => {
const folderPath = join(scenarioRoot, folderName);
const { filename, ...config } = await loadADLConfigInDir(folderPath);
return config;
};
const assertLoadFromFolder = async (folderName: string) => {
const config = await loadTestConfig(folderName);
deepStrictEqual(config, {
plugins: ["foo"],
diagnostics: [],
emitters: {
"foo:openapi": true,
},
lint: {
extends: [],
rules: {
"some-rule": "on",
},
},
});
};
it("loads yaml config file", async () => {
await assertLoadFromFolder("yaml");
});
it("loads json config file", async () => {
await assertLoadFromFolder("json");
});
it("loads from adl section in package.json config file", async () => {
await assertLoadFromFolder("package-json");
});
it("loads empty config if it can't find any config files", async () => {
const config = await loadTestConfig("empty");
deepStrictEqual(config, {
plugins: [],
diagnostics: [],
emitters: {},
lint: {
extends: [],
rules: {},
},
});
});
it("only loads first config file found", async () => {
// Should load .adlrc.yaml and MOT load .adlrc.json here
await assertLoadFromFolder("yaml-json");
});
it("deep clones defaults when not found", async () => {
let config = await loadTestConfig("empty");
config.plugins.push("x");
config.emitters["x"] = true;
config.lint.extends.push("x");
config.lint.rules["x"] = "off";
config = await loadTestConfig("empty");
deepStrictEqual(config, {
plugins: [],
diagnostics: [],
emitters: {},
lint: {
extends: [],
rules: {},
},
});
});
it("deep clones defaults when found", async () => {
let config = await loadTestConfig("yaml");
config.plugins.push("x");
config.emitters["x"] = true;
config.lint.extends.push("x");
config.lint.rules["x"] = "off";
config = await loadTestConfig("yaml");
deepStrictEqual(config, {
plugins: ["foo"],
diagnostics: [],
emitters: {
"foo:openapi": true,
},
lint: {
extends: [],
rules: {
"some-rule": "on",
},
},
});
});
});
describe("validation", () => {
const validator = new ConfigValidator();
const file = createSourceFile("<content>", "<path>");
function validate(data: ADLRawConfig) {
const diagnostics: Diagnostic[] = [];
validator.validateConfig(data, file, (d) => diagnostics.push(d));
return diagnostics;
}
it("does not allow additional properties", () => {
deepStrictEqual(validate({ someCustomProp: true } as any), [
{
file,
severity: "error",
message:
"Schema violation: must NOT have additional properties (/)\n additionalProperty: someCustomProp",
},
]);
});
it("fails if passing the wrong type", () => {
deepStrictEqual(validate({ emitters: true } as any), [
{
file,
severity: "error",
message: "Schema violation: must be object (/emitters)",
},
]);
});
it("succeeds if config is valid", () => {
deepStrictEqual(validate({ lint: { rules: { foo: "on" } } }), []);
});
});
});

Просмотреть файл

Просмотреть файл

@ -0,0 +1,11 @@
{
"plugins": ["foo"],
"emitters": {
"foo:openapi": true
},
"lint": {
"rules": {
"some-rule": "on"
}
}
}

Просмотреть файл

@ -0,0 +1,15 @@
{
"adl": {
"plugins": [
"foo"
],
"emitters": {
"foo:openapi": true
},
"lint": {
"rules": {
"some-rule": "on"
}
}
}
}

Просмотреть файл

@ -0,0 +1,6 @@
{
"plugins": ["otherconfig"],
"emitters": {
"foo:other": true
}
}

Просмотреть файл

@ -0,0 +1,10 @@
plugins:
- foo
# This has comments
emitters:
foo:openapi: true
lint:
rules:
"some-rule": "on"

Просмотреть файл

@ -0,0 +1,10 @@
plugins:
- foo
# This has comments
emitters:
foo:openapi: true
lint:
rules:
some-rule: on

Просмотреть файл

@ -3,7 +3,7 @@ import { ModelType } from "../../compiler/types.js";
import { getMaxValue, getMinValue } from "../../lib/decorators.js";
import { createTestHost, TestHost } from "../test-host.js";
describe("range limiting decorators", () => {
describe("adl: range limiting decorators", () => {
let testHost: TestHost;
beforeEach(async () => {
@ -12,7 +12,7 @@ describe("range limiting decorators", () => {
it("applies @minimum and @maximum decorators", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
@test model A { @minValue(15) foo: int32; @maxValue(55) boo: float32; }
@test model B { @maxValue(20) bar: int64; @minValue(23) car: float64; }
@ -21,9 +21,9 @@ describe("range limiting decorators", () => {
const { A, B } = (await testHost.compile("./")) as { A: ModelType; B: ModelType };
strictEqual(getMinValue(A.properties.get("foo")!), 15);
strictEqual(getMaxValue(A.properties.get("boo")!), 55);
strictEqual(getMaxValue(B.properties.get("bar")!), 20);
strictEqual(getMinValue(B.properties.get("car")!), 23);
strictEqual(getMinValue(testHost.program, A.properties.get("foo")!), 15);
strictEqual(getMaxValue(testHost.program, A.properties.get("boo")!), 55);
strictEqual(getMaxValue(testHost.program, B.properties.get("bar")!), 20);
strictEqual(getMinValue(testHost.program, B.properties.get("car")!), 23);
});
});

Просмотреть файл

@ -0,0 +1,508 @@
import { strictEqual, throws } from "assert";
import prettier from "prettier";
import * as plugin from "../../formatter/index.js";
function format(code: string): string {
const output = prettier.format(code, {
parser: "adl",
plugins: [plugin],
});
return output;
}
function assertFormat({ code, expected }: { code: string; expected: string }) {
const result = format(code);
strictEqual(result, expected.trim());
}
describe("adl: prettier formatter", () => {
it("throws error if there is a parsing issue", () => {
const code = `namespace this is invalid`;
throws(() => format(code));
});
it("format imports", () => {
assertFormat({
code: `
import "@azure-tools/adl-rest";
import "@azure-tools/adl-openapi";
import "@azure-tools/adl-rpaas" ;
`,
expected: `
import "@azure-tools/adl-rest";
import "@azure-tools/adl-openapi";
import "@azure-tools/adl-rpaas";
`,
});
});
describe("model", () => {
it("format simple models", () => {
assertFormat({
code: `
model Foo {
id: number;
type: Bar;
name?: string;
isArray: string[] ;
}
`,
expected: `
model Foo {
id: number;
type: Bar;
name?: string;
isArray: string[];
}
`,
});
});
it("format nested models", () => {
assertFormat({
code: `
model Foo {
id: number;
address: { street: string, country: string}
}
`,
expected: `
model Foo {
id: number;
address: {
street: string;
country: string;
};
}
`,
});
});
it("format models with spread", () => {
assertFormat({
code: `
model Foo {
id: number;
...Bar;
name: string;
}
`,
expected: `
model Foo {
id: number;
...Bar;
name: string;
}
`,
});
});
it("format model with decorator", () => {
assertFormat({
code: `
@some @decorator
model Foo {}
`,
expected: `
@some
@decorator
model Foo {}
`,
});
});
it("format model with heritage", () => {
assertFormat({
code: `
model Foo extends Base {
}
model Bar extends Base<
string > {
}
`,
expected: `
model Foo extends Base {}
model Bar extends Base<string> {}
`,
});
});
it("format model with generic", () => {
assertFormat({
code: `
model Foo < T >{
}
`,
expected: `
model Foo<T> {}
`,
});
});
});
describe("comments", () => {
it("format single line comments", () => {
assertFormat({
code: `
// This is a comment.
model Foo {}
`,
expected: `
// This is a comment.
model Foo {}
`,
});
});
it("format multi line comments", () => {
assertFormat({
code: `
/**
* This is a multiline comment
* that has bad formatting.
*/
model Foo {}
`,
expected: `
/**
* This is a multiline comment
* that has bad formatting.
*/
model Foo {}
`,
});
});
});
describe("alias union", () => {
it("format simple alias", () => {
assertFormat({
code: `
alias Foo = "one" | "two";
alias Bar
= "one"
| "two";
`,
expected: `
alias Foo = "one" | "two";
alias Bar = "one" | "two";
`,
});
});
it("format generic alias", () => {
assertFormat({
code: `
alias Foo< A, B> = A | B;
alias Bar<
A, B> =
A |
B;
`,
expected: `
alias Foo<A, B> = A | B;
alias Bar<A, B> = A | B;
`,
});
});
it("format long alias", () => {
assertFormat({
code: `
alias VeryLong = "one" | "two" | "three" | "four" | "five" | "six" | "seven" | "height" | "nine" | "ten";
`,
expected: `
alias VeryLong =
| "one"
| "two"
| "three"
| "four"
| "five"
| "six"
| "seven"
| "height"
| "nine"
| "ten";
`,
});
});
});
describe("alias intersection", () => {
it("format intersection of types", () => {
assertFormat({
code: `
alias Foo = One & Two;
alias Bar
= One &
Two;
`,
expected: `
alias Foo = One & Two;
alias Bar = One & Two;
`,
});
});
it("format intersection of anoymous models", () => {
assertFormat({
code: `
alias Foo = { foo: string } & {bar: string};
alias Bar
= { foo: string } &
{
bar: string};
`,
expected: `
alias Foo = {
foo: string;
} & {
bar: string;
};
alias Bar = {
foo: string;
} & {
bar: string;
};
`,
});
});
});
describe("enum", () => {
it("format simple enum", () => {
assertFormat({
code: `
enum Foo { A, B}
enum Bar
{ A,
B}
`,
expected: `
enum Foo {
A,
B,
}
enum Bar {
A,
B,
}
`,
});
});
it("format named enum", () => {
assertFormat({
code: `
enum Foo { A: "a", B : "b"}
enum Bar
{ A: "a",
B:
"b"}
`,
expected: `
enum Foo {
A: "a",
B: "b",
}
enum Bar {
A: "a",
B: "b",
}
`,
});
});
});
describe("namespaces", () => {
it("format global namespace", () => {
assertFormat({
code: `
namespace Foo;
`,
expected: `
namespace Foo;
`,
});
});
it("format global nested namespace", () => {
assertFormat({
code: `
namespace Foo . Bar;
`,
expected: `
namespace Foo.Bar;
`,
});
});
it("format global namespace with decorators", () => {
assertFormat({
code: `
@service
@other
namespace Foo . Bar;
`,
expected: `
@service
@other
namespace Foo.Bar;
`,
});
});
it("format namespace with body", () => {
assertFormat({
code: `
namespace Foo {
op some(): string;
}
namespace Foo . Bar {
op some(): string;
}
`,
expected: `
namespace Foo {
op some(): string;
}
namespace Foo.Bar {
op some(): string;
}
`,
});
});
it("format nested namespaces", () => {
assertFormat({
code: `
namespace Foo {
namespace Bar {
op some(): string;
}
}
`,
expected: `
namespace Foo {
namespace Bar {
op some(): string;
}
}
`,
});
});
});
describe("string literals", () => {
it("format single line string literal", () => {
assertFormat({
code: `
@doc( "this is a doc. "
)
model Foo {}
`,
expected: `
@doc("this is a doc. ")
model Foo {}
`,
});
});
it("format single line with newline characters", () => {
assertFormat({
code: `
@doc( "foo\\nbar"
)
model Foo {}
`,
expected: `
@doc("foo\\nbar")
model Foo {}
`,
});
});
it("format multi line string literal", () => {
assertFormat({
code: `
@doc( """
this is a doc.
that
span
multiple lines.
"""
)
model Foo {}
`,
expected: `
@doc("""
this is a doc.
that
span
multiple lines.
""")
model Foo {}
`,
});
});
});
describe("number literals", () => {
it("format integer", () => {
assertFormat({
code: `
alias MyNum = 123 ;
`,
expected: `
alias MyNum = 123;
`,
});
});
it("format float", () => {
assertFormat({
code: `
alias MyFloat1 = 1.234 ;
alias MyFloat2 = 0.123 ;
`,
expected: `
alias MyFloat1 = 1.234;
alias MyFloat2 = 0.123;
`,
});
});
it("format e notation numbers", () => {
assertFormat({
code: `
alias MyBigNumber = 1.0e8 ;
`,
expected: `
alias MyBigNumber = 1.0e8;
`,
});
});
it("format big numbers", () => {
assertFormat({
code: `
alias MyBigNumber = 1.0e999999999 ;
`,
expected: `
alias MyBigNumber = 1.0e999999999;
`,
});
});
});
});

Просмотреть файл

@ -0,0 +1,20 @@
alias UnionOfType = Foo | Bar;
alias UnionOfManyType = "one" | "two" | "three" | "four" | "five" | "six" | "seven" | "height" | "nine" | "ten";
alias UnionOfObject = {foo: string} | {bar: string};
alias UnionOfManyObject = {one: string} | {two: string} | {three: string} | {four: string} | {five: string};
alias UnionOfMix = Foo | { bar: string};
alias UnionOfManyMix = Foo | { bar: string} | Bar | Other | {other: string};
alias InterOfObject = {foo: string} & {bar: string};
alias InterOfManyObject = {one: string} & {two: string} & {three: string} & {four: string} & {five: string};
alias InterOfMix = Foo & { bar: string};
alias InterOfManyMix = Foo & { bar: string} & Bar & Other & {other: string};

Просмотреть файл

@ -0,0 +1,58 @@
import "@azure-tools/adl-rest";
import "@azure-tools/adl-openapi";
import "@azure-tools/adl-rpaas";
alias Bar = "that" | "this";
// This is a comment
alias VeryLong = "one" | "two" | "three" | "four" | "five" | "six" | "seven" | "height" | "nine" | "ten";
alias Inter = {foo: string} & {bar: string} ;
alias Inter2 = Foo &
Inter;
alias Generic< A, B> = A | B;
/**
* Model comment
*/
model Foo {
id: number;
type: Bar;
/*
* Name is special and comment is unaligned.
*/
name?: string;
isArray: string[] ;
address: { street: string, country: string};
...Bar;
}
@bar
enum SomeEnum { A, B, C}
enum
SomeNamedEnum { A: "a",
B: "b", @val() C: "c" }
/**
* Multi line comment still works,
* yeahh....
*/
@resource("/operations")
namespace Operations {
// Comment here too
@get @doc("Abc") op get(@path id: string): ArmResponse<OperationStatusResult>;
@get op list(): ArmResponse<OperationListResult> | ErrorResponse;
}

Просмотреть файл

@ -0,0 +1,11 @@
model Empty {
}
model WithProps {
foo: string;
bar: string
}
model GenericExtension<T> extends Parent<T> {
}

Просмотреть файл

@ -0,0 +1,80 @@
alias UnionOfType = Foo | Bar;
alias UnionOfManyType =
| "one"
| "two"
| "three"
| "four"
| "five"
| "six"
| "seven"
| "height"
| "nine"
| "ten";
alias UnionOfObject = {
foo: string;
} | {
bar: string;
};
alias UnionOfManyObject =
| {
one: string;
}
| {
two: string;
}
| {
three: string;
}
| {
four: string;
}
| {
five: string;
};
alias UnionOfMix = Foo | {
bar: string;
};
alias UnionOfManyMix =
| Foo
| {
bar: string;
}
| Bar
| Other
| {
other: string;
};
alias InterOfObject = {
foo: string;
} & {
bar: string;
};
alias InterOfManyObject = {
one: string;
} & {
two: string;
} & {
three: string;
} & {
four: string;
} & {
five: string;
};
alias InterOfMix = Foo & {
bar: string;
};
alias InterOfManyMix = Foo & {
bar: string;
} & Bar &
Other & {
other: string;
};

Просмотреть файл

@ -0,0 +1,71 @@
import "@azure-tools/adl-rest";
import "@azure-tools/adl-openapi";
import "@azure-tools/adl-rpaas";
alias Bar = "that" | "this";
// This is a comment
alias VeryLong =
| "one"
| "two"
| "three"
| "four"
| "five"
| "six"
| "seven"
| "height"
| "nine"
| "ten";
alias Inter = {
foo: string;
} & {
bar: string;
};
alias Inter2 = Foo & Inter;
alias Generic<A, B> = A | B;
/**
* Model comment
*/
model Foo {
id: number;
type: Bar;
/*
* Name is special and comment is unaligned.
*/
name?: string;
isArray: string[];
address: {
street: string;
country: string;
};
...Bar;
}
@bar
enum SomeEnum {
A,
B,
C,
}
enum SomeNamedEnum {
A: "a",
B: "b",
@val C: "c",
}
/**
* Multi line comment still works,
* yeahh....
*/
@resource("/operations")
namespace Operations {
// Comment here too
@get @doc("Abc") op get(@path id: string): ArmResponse<OperationStatusResult>;
@get op list(): ArmResponse<OperationListResult> | ErrorResponse;
}

Просмотреть файл

@ -0,0 +1,8 @@
model Empty {}
model WithProps {
foo: string;
bar: string;
}
model GenericExtension<T> extends Parent<T> {}

Просмотреть файл

@ -0,0 +1,67 @@
import { strictEqual } from "assert";
import { mkdir, readFile, writeFile } from "fs/promises";
import { dirname, join, resolve } from "path";
import prettier from "prettier";
import { fileURLToPath } from "url";
import * as plugin from "../../../formatter/index.js";
function format(code: string): string {
const output = prettier.format(code, {
parser: "adl",
plugins: [plugin],
});
return output;
}
const __dirname = dirname(fileURLToPath(import.meta.url));
const scenarioRoot = resolve(__dirname, "../../../../test/formatter/scenarios");
const shouldUpdate = process.argv.indexOf("--update-snapshots") !== -1;
async function getOutput(name: string): Promise<string | undefined> {
try {
const output = await readFile(join(scenarioRoot, "outputs", name));
return output.toString();
} catch {
return undefined;
}
}
async function saveOutput(name: string, content: string) {
const outputDir = join(scenarioRoot, "outputs");
await mkdir(outputDir, { recursive: true });
await writeFile(join(outputDir, name), content);
}
async function testScenario(name: string) {
const content = await readFile(join(scenarioRoot, "inputs", name));
const output = await getOutput(name);
const formatted = format(content.toString());
if (!output) {
return await saveOutput(name, formatted);
}
if (output !== formatted) {
if (shouldUpdate) {
return await saveOutput(name, formatted);
}
strictEqual(
formatted,
output,
`Scenario ${name} does not match expected snapshot. Run with tests '--update-snapshots' option to update.`
);
}
}
describe("adl: prettier formatter scenarios", () => {
it("misc", async () => {
await testScenario("misc.adl");
});
it("alias", async () => {
await testScenario("alias.adl");
});
it("model", async () => {
await testScenario("model.adl");
});
});

Просмотреть файл

@ -2,5 +2,5 @@ import "MyLib";
import "CustomAdlMain";
@myLibDec
model A = MyLib.Model;
model B = CustomAdlMain.Model;
model A { x: MyLib.Model };
model B { x: CustomAdlMain.Model };

Просмотреть файл

@ -4,7 +4,7 @@ import { NodeHost } from "../../compiler/util.js";
const libs = ["simple"];
describe("libraries", () => {
describe("adl: libraries", () => {
for (const lib of libs) {
describe(lib, () => {
it("compiles without error", async () => {
@ -12,10 +12,7 @@ describe("libraries", () => {
const mainFile = fileURLToPath(
new URL(`../../../test/libraries/${lib}/main.adl`, import.meta.url)
);
await createProgram(NodeHost, {
mainFile,
noEmit: true,
});
await createProgram(NodeHost, mainFile, { noEmit: true });
} catch (e) {
console.error(e.diagnostics);
throw e;

Просмотреть файл

@ -43,6 +43,11 @@ const weights = {
main();
function main() {
if (process.argv[2] !== "run") {
throw new Error(
"Correct usage is `node fuzz.js run`. Is there a missing/incorrect mocha exclude pattern causing this to load?"
);
}
const iterations = 10000;
console.log("Running parser fuzz test with 1000 iterations...");
fuzzTest(iterations);

Просмотреть файл

@ -1,34 +1,54 @@
import { readdir, readFile } from "fs/promises";
import { basename, isAbsolute, join, normalize, relative, resolve, sep } from "path";
import { basename, extname, isAbsolute, relative, resolve, sep } from "path";
import { fileURLToPath, pathToFileURL } from "url";
import { createProgram } from "../compiler/program.js";
import { CompilerHost, Type } from "../compiler/types";
import { formatDiagnostic, logDiagnostics, logVerboseTestOutput } from "../compiler/diagnostics.js";
import { CompilerOptions } from "../compiler/options.js";
import { createProgram, Program } from "../compiler/program.js";
import { CompilerHost, Diagnostic, Type } from "../compiler/types.js";
export interface TestHost {
addAdlFile(path: string, contents: string): void;
addJsFile(path: string, contents: any): void;
compile(main: string): Promise<Record<string, Type>>;
addRealAdlFile(path: string, realPath: string): Promise<void>;
addRealJsFile(path: string, realPath: string): Promise<void>;
compile(main: string, options?: CompilerOptions): Promise<Record<string, Type>>;
diagnose(main: string, options?: CompilerOptions): Promise<readonly Diagnostic[]>;
compileAndDiagnose(
main: string,
options?: CompilerOptions
): Promise<[Record<string, Type>, readonly Diagnostic[]]>;
testTypes: Record<string, Type>;
program: Program;
/**
* Virtual filesystem used in the tests.
*/
fs: { [name: string]: string };
fs: Map<string, string>;
}
class TestHostError extends Error {
constructor(message: string, public code: "ENOENT" | "ERR_MODULE_NOT_FOUND") {
super(message);
}
}
export async function createTestHost(): Promise<TestHost> {
const testTypes: Record<string, Type> = {};
const virtualFs: { [name: string]: string } = {};
const jsImports: { [path: string]: Promise<any> } = {};
let program: Program = undefined as any; // in practice it will always be initialized
const virtualFs = new Map<string, string>();
const jsImports = new Map<string, Promise<any>>();
const compilerHost: CompilerHost = {
async readFile(path: string) {
return virtualFs[path];
const contents = virtualFs.get(path);
if (contents === undefined) {
throw new TestHostError(`File ${path} not found.`, "ENOENT");
}
return contents;
},
async readDir(path: string) {
const contents = [];
for (const fsPath of Object.keys(virtualFs)) {
for (const fsPath of virtualFs.keys()) {
if (isContainedIn(path, fsPath)) {
contents.push({
isFile() {
@ -46,7 +66,7 @@ export async function createTestHost(): Promise<TestHost> {
},
async writeFile(path: string, content: string) {
virtualFs[path] = content;
virtualFs.set(path, content);
},
getLibDirs() {
@ -58,7 +78,11 @@ export async function createTestHost(): Promise<TestHost> {
},
getJsImport(path) {
return jsImports[path];
const module = jsImports.get(path);
if (module === undefined) {
throw new TestHostError(`Module ${path} not found`, "ERR_MODULE_NOT_FOUND");
}
return module;
},
getCwd() {
@ -66,7 +90,18 @@ export async function createTestHost(): Promise<TestHost> {
},
async stat(path: string) {
for (const fsPath of Object.keys(virtualFs)) {
if (virtualFs.has(path)) {
return {
isDirectory() {
return false;
},
isFile() {
return true;
},
};
}
for (const fsPath of virtualFs.keys()) {
if (fsPath.startsWith(path) && fsPath !== path) {
return {
isDirectory() {
@ -79,14 +114,7 @@ export async function createTestHost(): Promise<TestHost> {
}
}
return {
isDirectory() {
return false;
},
isFile() {
return true;
},
};
throw new TestHostError(`File ${path} not found`, "ENOENT");
},
// symlinks not supported in test-host
@ -96,31 +124,42 @@ export async function createTestHost(): Promise<TestHost> {
};
// load standard library into the vfs
for (const relDir of ["../../lib", "../../../lib"]) {
for (const [relDir, virtualDir] of [
["../../lib", "/.adl/dist/lib"],
["../../../lib", "/.adl/lib"],
]) {
const dir = resolve(fileURLToPath(import.meta.url), relDir);
const contents = await readdir(dir, { withFileTypes: true });
for (const entry of contents) {
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const realPath = resolve(dir, entry.name);
const virtualPath = resolve(virtualDir, entry.name);
if (entry.isFile()) {
const path = join(dir, entry.name);
const virtualDir = compilerHost.getLibDirs()[0];
const key = normalize(join(virtualDir, entry.name));
if (entry.name.endsWith(".js")) {
jsImports[key] = import(pathToFileURL(path).href);
virtualFs[key] = ""; // don't need contents.
} else {
const contents = await readFile(path, "utf-8");
virtualFs[key] = contents;
switch (extname(entry.name)) {
case ".adl":
const contents = await readFile(realPath, "utf-8");
virtualFs.set(virtualPath, contents);
break;
case ".js":
case ".mjs":
jsImports.set(virtualPath, import(pathToFileURL(realPath).href));
virtualFs.set(virtualPath, ""); // don't need contents.
break;
}
}
}
}
// add test decorators
addAdlFile("/.adl/test-lib/main.adl", 'import "./test.js";');
addJsFile("/.adl/test-lib/test.js", {
test(_: any, target: Type, name?: string) {
if (!name) {
if (target.kind === "Model" || target.kind === "Namespace") {
if (
target.kind === "Model" ||
target.kind === "Namespace" ||
target.kind === "Enum" ||
target.kind === "Operation"
) {
name = target.name;
} else {
throw new Error("Need to specify a name for test type");
@ -134,33 +173,66 @@ export async function createTestHost(): Promise<TestHost> {
return {
addAdlFile,
addJsFile,
addRealAdlFile,
addRealJsFile,
compile,
diagnose,
compileAndDiagnose,
testTypes,
get program() {
return program;
},
fs: virtualFs,
};
function addAdlFile(path: string, contents: string) {
virtualFs[resolve(compilerHost.getCwd(), path)] = contents;
virtualFs.set(resolve(compilerHost.getCwd(), path), contents);
}
function addJsFile(path: string, contents: any) {
const key = resolve(compilerHost.getCwd(), path);
// don't need file contents;
virtualFs[key] = "";
jsImports[key] = new Promise((r) => r(contents));
virtualFs.set(key, ""); // don't need contents
jsImports.set(key, new Promise((r) => r(contents)));
}
async function compile(main: string) {
try {
const program = await createProgram(compilerHost, {
mainFile: main,
noEmit: true,
});
async function addRealAdlFile(path: string, existingPath: string) {
virtualFs.set(resolve(compilerHost.getCwd(), path), await readFile(existingPath, "utf8"));
}
return testTypes;
} catch (e) {
throw e;
async function addRealJsFile(path: string, existingPath: string) {
const key = resolve(compilerHost.getCwd(), path);
const exports = await import(pathToFileURL(existingPath).href);
virtualFs.set(key, "");
jsImports.set(key, exports);
}
async function compile(main: string, options: CompilerOptions = {}) {
const [testTypes, diagnostics] = await compileAndDiagnose(main, options);
if (diagnostics.length > 0) {
let message = "Unexpected diagnostics:\n" + diagnostics.map(formatDiagnostic).join("\n");
throw new Error(message);
}
return testTypes;
}
async function diagnose(main: string, options: CompilerOptions = {}) {
const [, diagnostics] = await compileAndDiagnose(main, options);
return diagnostics;
}
async function compileAndDiagnose(
mainFile: string,
options: CompilerOptions = {}
): Promise<[Record<string, Type>, readonly Diagnostic[]]> {
if (options.noEmit === undefined) {
// default for tests is noEmit
options = { ...options, noEmit: true };
}
program = await createProgram(compilerHost, mainFile, options);
logVerboseTestOutput((log) => logDiagnostics(program.diagnostics, log));
return [testTypes, program.diagnostics];
}
function isContainedIn(a: string, b: string) {

Просмотреть файл

@ -0,0 +1,120 @@
import { strictEqual } from "assert";
import {
addModelProperty,
addOperationParameter,
addOperationResponseType,
} from "../compiler/mutators.js";
import { Program } from "../compiler/program.js";
import { ModelType, OperationType, UnionType } from "../compiler/types.js";
import { createTestHost, TestHost } from "./test-host.js";
describe("adl: mutators", () => {
let testHost: TestHost;
beforeEach(async () => {
testHost = await createTestHost();
});
function addBarProperty(program: Program, model: ModelType) {
addModelProperty(program, model, "bar", "string");
}
it("addModelProperty adds a property to a model type", async () => {
testHost.addJsFile("a.js", { addBarProperty });
testHost.addAdlFile(
"main.adl",
`
import "./a.js";
@test
@addBarProperty
model A { foo: int32; }
`
);
const { A } = (await testHost.compile("./")) as { A: ModelType };
strictEqual(A.properties.size, 2);
strictEqual(A.properties.get("bar")!.name, "bar");
strictEqual((A.properties.get("bar")!.type as ModelType).name, "string");
});
function addParameters(program: Program, operation: OperationType) {
addOperationParameter(program, operation, "omega", "string");
addOperationParameter(program, operation, "alpha", "int64", { insertIndex: 0 });
addOperationParameter(program, operation, "beta", "B.Excellent", { insertIndex: 1 });
}
it("addOperationParameter inserts operation parameters", async () => {
testHost.addJsFile("a.js", { addParameters });
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
import "./a.js";
import "./b.adl";
@test
@addParameters
op TestOp(foo: int32): string;
`
);
testHost.addAdlFile(
"b.adl",
`
namespace B;
model Excellent {}
`
);
const { TestOp } = (await testHost.compile("./")) as { TestOp: OperationType };
const params = Array.from(TestOp.parameters.properties.entries());
strictEqual(TestOp.parameters.properties.size, 4);
strictEqual(params[0][0], "alpha");
strictEqual((params[0][1].type as ModelType).name, "int64");
strictEqual(params[1][0], "beta");
strictEqual((params[1][1].type as ModelType).name, "Excellent");
strictEqual(params[2][0], "foo");
strictEqual((params[2][1].type as ModelType).name, "int32");
strictEqual(params[3][0], "omega");
strictEqual((params[3][1].type as ModelType).name, "string");
});
function addResponseTypes(program: Program, operation: OperationType) {
addOperationResponseType(program, operation, "int64");
addOperationResponseType(program, operation, "A.Response");
}
it("addModelProperty adds a property to a model type", async () => {
testHost.addJsFile("a.js", { addResponseTypes });
testHost.addAdlFile(
"main.adl",
`
import "./a.js";
@test
@addResponseTypes
op TestOp(foo: int32): string;
namespace A {
model Response {}
}
`
);
const { TestOp } = (await testHost.compile("./")) as { TestOp: OperationType };
const unionType = TestOp.returnType as UnionType;
strictEqual(unionType.options.length, 3);
strictEqual((unionType.options[0] as ModelType).name, "string");
strictEqual((unionType.options[1] as ModelType).name, "int64");
strictEqual((unionType.options[2] as ModelType).name, "Response");
});
});

Просмотреть файл

@ -1,9 +1,10 @@
import assert from "assert";
import { logDiagnostics, logVerboseTestOutput } from "../compiler/diagnostics.js";
import { CharCode } from "../compiler/charcode.js";
import { formatDiagnostic, logDiagnostics, logVerboseTestOutput } from "../compiler/diagnostics.js";
import { hasParseError, NodeFlags, parse } from "../compiler/parser.js";
import { ADLScriptNode, SyntaxKind } from "../compiler/types.js";
describe("syntax", () => {
describe("adl: syntax", () => {
describe("import statements", () => {
parseEach(['import "x";']);
@ -101,7 +102,11 @@ describe("syntax", () => {
});
describe("model = statements", () => {
parseEach(["model x = y;", "model foo = bar | baz;", "model bar<a, b> = a | b;"]);
parseErrorEach([
["model x = y;", [/'{' expected/]],
["model foo = bar | baz;", [/'{' expected/]],
["model bar<a, b> = a | b;", [/'{' expected/]],
]);
});
describe("model expressions", () => {
@ -121,7 +126,7 @@ describe("syntax", () => {
});
describe("template instantiations", () => {
parseEach(["model A = Foo<number, string>;", "model B = Foo<number, string>[];"]);
parseEach(["model A { x: Foo<number, string>; }", "model B { x: Foo<number, string>[]; }"]);
});
describe("intersection expressions", () => {
@ -129,7 +134,7 @@ describe("syntax", () => {
});
describe("parenthesized expressions", () => {
parseEach(["model A = ((B | C) & D)[];"]);
parseEach(["model A { x: ((B | C) & D)[]; }"]);
});
describe("namespace statements", () => {
@ -165,7 +170,6 @@ describe("syntax", () => {
`
model A { };
model B { }
model C = A;
;
namespace I {
op foo(): number;
@ -219,53 +223,248 @@ describe("syntax", () => {
]);
});
describe("non-ascii identifiers", () => {
parseEach(["model Incompréhensible {}", "model 𐌰𐌲 {}", "model Banana𐌰𐌲Banana {}"]);
parseErrorEach([["model 😢 {}", [/Invalid character/]]]);
describe("unterminated tokens", () => {
parseErrorEach([["/* Yada yada yada", [/Unterminated multi-line comment/]]]);
const strings = [
'"banana',
'"banana\\',
'"banana\r"',
'"banana\n"',
'"banana\r\n"',
'"banana\u{2028}"',
'"banana\u{2029}"',
'"""\nbanana',
'"""\nbanana\\',
];
parseErrorEach(
Array.from(strings.entries()).map((e) => [
`alias ${String.fromCharCode(CharCode.A + e[0])} = ${e[1]}`,
[/Unterminated string literal/],
(node) => {
const statement = node.statements[0];
assert(statement.kind === SyntaxKind.AliasStatement, "alias statement expected");
const value = statement.value;
assert(value.kind === SyntaxKind.StringLiteral, "string literal expected");
assert.strictEqual(value.value, "banana");
},
])
);
});
describe("terminated tokens at EOF", () => {
parseErrorEach([
["alias X = 0x10101", [/';' expected/]],
["alias X = 0xBEEF", [/';' expected/]],
["alias X = 123", [/';' expected/]],
["alias X = 123e45", [/';' expected/]],
["alias X = 123.45", [/';' expected/]],
["alias X = 123.45e2", [/';' expected/]],
["alias X = Banana", [/';' expected/]],
['alias X = "Banana"', [/';' expected/]],
['alias X = """\nBanana\n"""', [/';' expected/]],
]);
});
describe("numeric literals", () => {
const good: [string, number][] = [
// Some questions remain here: https://github.com/Azure/adl/issues/506
["-0", -0],
["1e9999", Infinity],
["1e-9999", 0],
["-1e-9999", -0],
["-1e9999", -Infinity],
// NOTE: No octal in ADL
["077", 77],
["+077", 77],
["-077", -77],
["0xABCD", 0xabcd],
["0xabcd", 0xabcd],
["0x1010", 0x1010],
["0b1010", 0b1010],
["0", 0],
["+0", 0],
["0.0", 0.0],
["+0.0", 0],
["-0.0", -0.0],
["123", 123],
["+123", 123],
["-123", -123],
["123.123", 123.123],
["+123.123", 123.123],
["-123.123", -123.123],
["789e42", 789e42],
["+789e42", 789e42],
["-789e42", -789e42],
["654.321e9", 654.321e9],
["+654.321e9", 654.321e9],
["-654.321e9", -654.321e9],
];
const bad: [string, RegExp][] = [
["123.", /Digit expected/],
["123.0e", /Digit expected/],
["123e", /Digit expected/],
["0b", /Binary digit expected/],
["0b2", /Binary digit expected/],
["0x", /Hexadecimal digit expected/],
["0xG", /Hexadecimal digit expected/],
];
parseEach(good.map((c) => [`alias M = ${c[0]};`, (node) => isNumericLiteral(node, c[1])]));
parseErrorEach(bad.map((c) => [`alias M = ${c[0]};`, [c[1]]]));
function isNumericLiteral(node: ADLScriptNode, value: number) {
const statement = node.statements[0];
assert(statement.kind === SyntaxKind.AliasStatement, "alias statement expected");
const assignment = statement.value;
assert(assignment?.kind === SyntaxKind.NumericLiteral, "numeric literal expected");
assert.strictEqual(assignment.value, value);
}
});
describe("identifiers", () => {
const good = [
"short",
"short42",
"lowercaseandlong",
"lowercaseandlong42",
"camelCase",
"camelCase42",
"PascalCase",
"PascalCase42",
"has_underscore",
"has_$dollar",
"_startsWithUnderscore",
"$startsWithDollar",
"Incompréhensible",
"incompréhensible",
"IncomprÉhensible",
"incomprÉhensible",
// leading astral character
"𐌰𐌲",
// continuing astral character
"Banana𐌰𐌲42Banana",
"banana𐌰𐌲42banana",
// ZWNJ
"deaf\u{200c}ly",
// ZWJ
"क्‍ष",
];
const bad: [string, RegExp][] = [
["😢", /Invalid character/],
["42", /Identifier expected/],
["true", /Keyword cannot be used as identifier/],
];
parseEach(
good.map((s) => [
`model ${s} {}`,
(node) => {
const statement = node.statements[0];
assert(statement.kind === SyntaxKind.ModelStatement, "Model statement expected.");
assert.strictEqual(statement.id.sv, s);
},
])
);
parseErrorEach(bad.map((e) => [`model ${e[0]} {}`, [e[1]]]));
});
// smaller repro of previous regen-samples baseline failures
describe("sample regressions", () => {
parseEach([
[
`/* \\n <-- before string! */ @format("\\\\w") model M {}`,
(node) => {
assert(node.statements[0].kind === SyntaxKind.ModelStatement);
assert(node.statements[0].decorators[0].arguments[0].kind === SyntaxKind.StringLiteral);
assert.strictEqual(node.statements[0].decorators[0].arguments[0].value, "\\w");
},
],
]);
});
describe("enum statements", () => {
parseEach([
"enum Foo { }",
"enum Foo { a, b }",
'enum Foo { a: "hi", c: 10 }',
"@foo enum Foo { @bar a, @baz b: 10 }",
]);
parseErrorEach([
["enum Foo { a: number }", [/Expected numeric or string literal/]],
["enum Foo { a: ; b: ; }", [/Expression expected/, /Expression expected/]],
["enum Foo { ;+", [/Enum member expected/]],
["enum { }", [/Identifier expected/]],
]);
});
describe("alias statements", () => {
parseEach(["alias X = 1;", "alias X = A | B;", "alias MaybeUndefined<T> = T | undefined;"]);
parseErrorEach([
["@foo alias Bar = 1;", [/Cannot decorate alias statement/]],
["alias Foo =", [/Expression expected/]],
["alias Foo<> =", [/Identifier expected/, /Expression expected/]],
["alias Foo<T> = X |", [/Expression expected/]],
["alias =", [/Identifier expected/]],
]);
});
});
function parseEach(cases: string[]) {
for (const code of cases) {
type Callback = (node: ADLScriptNode) => void;
function parseEach(cases: (string | [string, Callback])[]) {
for (const each of cases) {
const code = typeof each === "string" ? each : each[0];
const callback = typeof each === "string" ? undefined : each[1];
it("parses `" + shorten(code) + "`", () => {
logVerboseTestOutput("=== Source ===");
logVerboseTestOutput(code);
logVerboseTestOutput("\n=== Parse Result ===");
const astNode = parse(code);
if (callback) {
callback(astNode);
}
dumpAST(astNode);
logVerboseTestOutput("\n=== Diagnostics ===");
if (astNode.parseDiagnostics.length > 0) {
const diagnostics: string[] = [];
logDiagnostics(astNode.parseDiagnostics, (e) => diagnostics.push(e!));
const diagnostics = astNode.parseDiagnostics.map(formatDiagnostic).join("\n");
assert.strictEqual(
hasParseError(astNode),
astNode.parseDiagnostics.some((e) => e.severity === "error"),
"root node claims to have no parse errors, but these were reported:\n" +
diagnostics.join("\n")
"root node claims to have no parse errors, but these were reported:\n" + diagnostics
);
assert.fail("Unexpected parse errors in test:\n" + diagnostics.join("\n"));
assert.fail("Unexpected parse errors in test:\n" + diagnostics);
}
});
}
}
function parseErrorEach(cases: [string, RegExp[]][]) {
for (const [code, matches] of cases) {
function parseErrorEach(cases: [string, RegExp[], Callback?][], significantWhitespace = false) {
for (const [code, matches, callback] of cases) {
it(`doesn't parse ${shorten(code)}`, () => {
logVerboseTestOutput("=== Source ===");
logVerboseTestOutput(code);
const astNode = parse(code);
if (callback) {
callback(astNode);
}
logVerboseTestOutput("\n=== Parse Result ===");
dumpAST(astNode);
logVerboseTestOutput("\n=== Diagnostics ===");
logVerboseTestOutput((log) => logDiagnostics(astNode.parseDiagnostics, log));
assert.notStrictEqual(astNode.parseDiagnostics.length, 0);
assert.notStrictEqual(astNode.parseDiagnostics.length, 0, "parse diagnostics length");
let i = 0;
for (const match of matches) {
assert.match(astNode.parseDiagnostics[i++].message, match);
@ -280,7 +479,7 @@ function parseErrorEach(cases: [string, RegExp[]][]) {
function dumpAST(astNode: ADLScriptNode) {
logVerboseTestOutput((log) => {
const hasErrors = hasParseError(astNode); // force flags to initialize
hasParseError(astNode); // force flags to initialize
const json = JSON.stringify(astNode, replacer, 2);
log(json);
});

Просмотреть файл

@ -1,23 +1,37 @@
import assert from "assert";
import { readFile } from "fs/promises";
import { URL } from "url";
import { throwOnError } from "../compiler/diagnostics.js";
import { isIdentifierContinue, isIdentifierStart } from "../compiler/charcode.js";
import { DiagnosticHandler, formatDiagnostic } from "../compiler/diagnostics.js";
import {
createScanner,
isKeyword,
isPunctuation,
isStatementKeyword,
KeywordLimit,
Keywords,
maxKeywordLength,
Token,
TokenDisplay,
} from "../compiler/scanner.js";
import { LineAndCharacter } from "../compiler/types.js";
type TokenEntry = [Token, string?, number?, LineAndCharacter?];
type TokenEntry = [
Token,
string?,
{
pos?: number;
line?: number;
character?: number;
value?: string;
}?
];
function tokens(text: string, onError = throwOnError): TokenEntry[] {
const scanner = createScanner(text, onError);
function tokens(text: string, diagnosticHandler?: DiagnosticHandler): TokenEntry[] {
if (!diagnosticHandler) {
diagnosticHandler = (diagnostic) =>
assert.fail("Unexpected diagnostic: " + formatDiagnostic(diagnostic));
}
const scanner = createScanner(text, diagnosticHandler);
const result: TokenEntry[] = [];
do {
const token = scanner.scan();
@ -25,8 +39,11 @@ function tokens(text: string, onError = throwOnError): TokenEntry[] {
result.push([
scanner.token,
scanner.getTokenText(),
scanner.tokenPosition,
scanner.file.getLineAndCharacterOfPosition(scanner.tokenPosition),
{
pos: scanner.tokenPosition,
value: scanner.getTokenValue(),
...scanner.file.getLineAndCharacterOfPosition(scanner.tokenPosition),
},
]);
} while (!scanner.eof());
@ -38,44 +55,61 @@ function tokens(text: string, onError = throwOnError): TokenEntry[] {
}
function verify(tokens: TokenEntry[], expecting: TokenEntry[]) {
for (const [
index,
[expectedToken, expectedText, expectedPosition, expectedLineAndCharacter],
] of expecting.entries()) {
const [token, text, position, lineAndCharacter] = tokens[index];
for (const [index, [expectedToken, expectedText, expectedAdditional]] of expecting.entries()) {
const [token, text, additional] = tokens[index];
assert.strictEqual(Token[token], Token[expectedToken], `Token ${index} must match`);
if (expectedText) {
assert.strictEqual(text, expectedText, `Token ${index} test must match`);
}
if (expectedPosition) {
assert.strictEqual(position, expectedPosition, `Token ${index} position must match`);
if (expectedAdditional?.pos) {
assert.strictEqual(
additional!.pos,
expectedAdditional.pos,
`Token ${index} position must match`
);
}
if (expectedLineAndCharacter) {
assert.deepStrictEqual(
lineAndCharacter,
expectedLineAndCharacter,
`Token ${index} line and character must match`
if (expectedAdditional?.line) {
assert.strictEqual(
additional!.line,
expectedAdditional.line,
`Token ${index} line must match`
);
}
if (expectedAdditional?.character) {
assert.strictEqual(
additional!.character,
expectedAdditional?.character,
`Token ${index} character must match`
);
}
if (expectedAdditional?.value) {
assert.strictEqual(
additional!.value,
expectedAdditional.value,
`Token ${index} value must match`
);
}
}
}
describe("scanner", () => {
describe("adl: scanner", () => {
/** verifies that we can scan tokens and get back some output. */
it("smoketest", () => {
const all = tokens("\tthis is a test");
const all = tokens('\tthis is "a" test');
verify(all, [
[Token.Whitespace],
[Token.Identifier, "this"],
[Token.Identifier, "this", { value: "this" }],
[Token.Whitespace],
[Token.Identifier, "is"],
[Token.Identifier, "is", { value: "is" }],
[Token.Whitespace],
[Token.Identifier, "a"],
[Token.StringLiteral, '"a"', { value: "a" }],
[Token.Whitespace],
[Token.Identifier, "test"],
[Token.Identifier, "test", { value: "test" }],
]);
});
@ -94,6 +128,11 @@ describe("scanner", () => {
]);
});
it("scans intersections", () => {
const all = tokens("A&B");
verify(all, [[Token.Identifier, "A"], [Token.Ampersand], [Token.Identifier, "B"]]);
});
it("scans decorator expressions", () => {
const all = tokens('@foo(1,"hello",foo)');
@ -146,11 +185,20 @@ describe("scanner", () => {
]);
});
function scanString(text: string, expectedValue: string) {
const scanner = createScanner(text);
function scanString(text: string, expectedValue: string, expectedDiagnostic?: RegExp) {
const scanner = createScanner(text, (diagnostic) => {
if (expectedDiagnostic) {
assert.match(diagnostic.message, expectedDiagnostic);
} else {
assert.fail("No diagnostic expected, but got " + formatDiagnostic(diagnostic));
}
});
assert.strictEqual(scanner.scan(), Token.StringLiteral);
assert.strictEqual(scanner.token, Token.StringLiteral);
assert.strictEqual(scanner.getTokenText(), text);
if (!expectedDiagnostic) {
assert.strictEqual(scanner.getTokenText(), text);
}
assert.strictEqual(scanner.getTokenValue(), expectedValue);
}
@ -162,56 +210,64 @@ describe("scanner", () => {
scanString('"Hello world \\r\\n \\t \\" \\\\ !"', 'Hello world \r\n \t " \\ !');
});
it("scans multi-line strings", () => {
scanString('"More\r\nthan\r\none\r\nline"', "More\nthan\none\nline");
it("does not allow multi-line, non-triple-quoted strings", () => {
scanString('"More\r\nthan\r\none\r\nline"', "More", /Unterminated string/);
scanString('"More\nthan\none\nline"', "More", /Unterminated string/);
scanString('"Fancy\u{2028}line separator"', "Fancy", /Unterminated string/);
scanString('"Fancy\u{2029}paragraph separator', "Fancy", /Unterminated string/);
});
it("scans triple-quoted strings", () => {
scanString(
// NOTE: sloppy blank line formatting and trailing whitespace after open
// quotes above is deliberate here and deliberately tolerated by
// the scanner.
`"""
This is a triple-quoted string
And this is another line
"You do not need to escape lone quotes"
You can use escape sequences: \\r \\n \\t \\\\ \\"
"""`,
// NOTE: sloppy blank line formatting and trailing whitespace after open
// quotes above is deliberately tolerated.
"This is a triple-quoted string\n\n\n\nAnd this is another line"
'This is a triple-quoted string\n\n\n"You do not need to escape lone quotes"\nYou can use escape sequences: \r \n \t \\ "'
);
});
it("normalizes CRLF to LF in multi-line string", () => {
scanString('"""\r\nThis\r\nis\r\na\r\ntest\r\n"""', "This\nis\na\ntest");
});
it("provides token position", () => {
const all = tokens("a x\raa x\r\naaa x\naaaa x\u{2028}aaaaa x\u{2029}aaaaaa x");
verify(all, [
[Token.Identifier, "a", 0, { line: 0, character: 0 }],
[Token.Whitespace, " ", 1, { line: 0, character: 1 }],
[Token.Identifier, "x", 2, { line: 0, character: 2 }],
[Token.NewLine, "\r", 3, { line: 0, character: 3 }],
[Token.Identifier, "a", { pos: 0, line: 0, character: 0 }],
[Token.Whitespace, " ", { pos: 1, line: 0, character: 1 }],
[Token.Identifier, "x", { pos: 2, line: 0, character: 2 }],
[Token.NewLine, "\r", { pos: 3, line: 0, character: 3 }],
[Token.Identifier, "aa", 4, { line: 1, character: 0 }],
[Token.Whitespace, " ", 6, { line: 1, character: 2 }],
[Token.Identifier, "x", 7, { line: 1, character: 3 }],
[Token.NewLine, "\r\n", 8, { line: 1, character: 4 }],
[Token.Identifier, "aa", { pos: 4, line: 1, character: 0 }],
[Token.Whitespace, " ", { pos: 6, line: 1, character: 2 }],
[Token.Identifier, "x", { pos: 7, line: 1, character: 3 }],
[Token.NewLine, "\r\n", { pos: 8, line: 1, character: 4 }],
[Token.Identifier, "aaa", 10, { line: 2, character: 0 }],
[Token.Whitespace, " ", 13, { line: 2, character: 3 }],
[Token.Identifier, "x", 14, { line: 2, character: 4 }],
[Token.NewLine, "\n", 15, { line: 2, character: 5 }],
[Token.Identifier, "aaa", { pos: 10, line: 2, character: 0 }],
[Token.Whitespace, " ", { pos: 13, line: 2, character: 3 }],
[Token.Identifier, "x", { pos: 14, line: 2, character: 4 }],
[Token.NewLine, "\n", { pos: 15, line: 2, character: 5 }],
[Token.Identifier, "aaaa", 16, { line: 3, character: 0 }],
[Token.Whitespace, " ", 20, { line: 3, character: 4 }],
[Token.Identifier, "x", 21, { line: 3, character: 5 }],
[Token.NewLine, "\u{2028}", 22, { line: 3, character: 6 }],
[Token.Identifier, "aaaa", { pos: 16, line: 3, character: 0 }],
[Token.Whitespace, " ", { pos: 20, line: 3, character: 4 }],
[Token.Identifier, "x", { pos: 21, line: 3, character: 5 }],
[Token.NewLine, "\u{2028}", { pos: 22, line: 3, character: 6 }],
[Token.Identifier, "aaaaa", 23, { line: 4, character: 0 }],
[Token.Whitespace, " ", 28, { line: 4, character: 5 }],
[Token.Identifier, "x", 29, { line: 4, character: 6 }],
[Token.NewLine, "\u{2029}", 30, { line: 4, character: 7 }],
[Token.Identifier, "aaaaa", { pos: 23, line: 4, character: 0 }],
[Token.Whitespace, " ", { pos: 28, line: 4, character: 5 }],
[Token.Identifier, "x", { pos: 29, line: 4, character: 6 }],
[Token.NewLine, "\u{2029}", { pos: 30, line: 4, character: 7 }],
[Token.Identifier, "aaaaaa", 31, { line: 5, character: 0 }],
[Token.Whitespace, " ", 37, { line: 5, character: 6 }],
[Token.Identifier, "x", 38, { line: 5, character: 7 }],
[Token.Identifier, "aaaaaa", { pos: 31, line: 5, character: 0 }],
[Token.Whitespace, " ", { pos: 37, line: 5, character: 6 }],
[Token.Identifier, "x", { pos: 38, line: 5, character: 7 }],
]);
});
@ -225,11 +281,20 @@ describe("scanner", () => {
`Token enum has ${tokenCount} elements but TokenDisplay array has ${tokenDisplayCount}.`
);
// check that keywords have appropriate display
// check that keywords have appropriate display and limits
const nonStatementKeywords = [Token.ExtendsKeyword, Token.TrueKeyword, Token.FalseKeyword];
let maxKeywordLengthFound = -1;
for (const [name, token] of Keywords.entries()) {
let minKeywordLengthFound = Number.MAX_SAFE_INTEGER;
let maxKeywordLengthFound = Number.MIN_SAFE_INTEGER;
for (const [name, token] of Keywords) {
assert.match(
name,
/^[a-z]+$/,
"We need to change the keyword lookup algorithm in the scanner if we ever add a keyword that is not all lowercase ascii letters."
);
minKeywordLengthFound = Math.min(minKeywordLengthFound, name.length);
maxKeywordLengthFound = Math.max(maxKeywordLengthFound, name.length);
assert.strictEqual(TokenDisplay[token], `'${name}'`);
assert(isKeyword(token), `${name} should be classified as a keyword`);
if (!nonStatementKeywords.includes(token)) {
@ -237,7 +302,21 @@ describe("scanner", () => {
}
}
assert.strictEqual(maxKeywordLengthFound, maxKeywordLength);
assert.strictEqual(
minKeywordLengthFound,
KeywordLimit.MinLength,
`min keyword length is incorrect, set KeywordLimit.MinLength to ${minKeywordLengthFound}`
);
assert.strictEqual(
maxKeywordLengthFound,
KeywordLimit.MaxLength,
`max keyword length is incorrect, set KeywordLimit.MaxLength to ${maxKeywordLengthFound}`
);
assert(
maxKeywordLengthFound < 11,
"We need to change the keyword lookup algorithm in the scanner if we ever add a keyword with 11 characters or more."
);
// check single character punctuation
for (let i = 33; i <= 126; i++) {
@ -256,15 +335,63 @@ describe("scanner", () => {
// check the rest
assert.strictEqual(TokenDisplay[Token.Elipsis], "'...'");
assert.strictEqual(TokenDisplay[Token.None], "<none>");
assert.strictEqual(TokenDisplay[Token.Invalid], "<invalid>");
assert.strictEqual(TokenDisplay[Token.EndOfFile], "<end of file>");
assert.strictEqual(TokenDisplay[Token.SingleLineComment], "<single-line comment>");
assert.strictEqual(TokenDisplay[Token.MultiLineComment], "<multi-line comment>");
assert.strictEqual(TokenDisplay[Token.NewLine], "<newline>");
assert.strictEqual(TokenDisplay[Token.Whitespace], "<whitespace>");
assert.strictEqual(TokenDisplay[Token.ConflictMarker], "<conflict marker>");
assert.strictEqual(TokenDisplay[Token.Identifier], "<identifier>");
assert.strictEqual(TokenDisplay[Token.None], "none");
assert.strictEqual(TokenDisplay[Token.Invalid], "invalid");
assert.strictEqual(TokenDisplay[Token.EndOfFile], "end of file");
assert.strictEqual(TokenDisplay[Token.SingleLineComment], "single-line comment");
assert.strictEqual(TokenDisplay[Token.MultiLineComment], "multi-line comment");
assert.strictEqual(TokenDisplay[Token.NewLine], "newline");
assert.strictEqual(TokenDisplay[Token.Whitespace], "whitespace");
assert.strictEqual(TokenDisplay[Token.ConflictMarker], "conflict marker");
assert.strictEqual(TokenDisplay[Token.Identifier], "identifier");
});
// Search for Other_ID_Start in https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt
const otherIDStart = [0x1885, 0x1886, 0x2118, 0x212e, 0x309b, 0x309c];
// Search for Other_ID_Continue in https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt
const otherIdContinue = [
0x00b7,
0x0387,
0x1369,
0x136a,
0x136b,
0x136c,
0x136d,
0x136e,
0x136f,
0x1370,
0x1371,
0x19da,
];
it("allows additional identifier start characters", () => {
assert(isIdentifierStart("$".codePointAt(0)!), "'$' should be allowed to start identifier.");
assert(isIdentifierStart("_".codePointAt(0)!), "'_' should be allowed to start identifier.");
for (const codePoint of otherIDStart) {
assert(
isIdentifierStart(codePoint),
`U+${codePoint.toString(16)} should be allowed to start identifier.`
);
}
});
it("allows additional identifier continuation characters", () => {
//prettier-ignore
assert(isIdentifierContinue("$".codePointAt(0)!), "'$' should be allowed to continue identifier.");
//prettier-ignore
assert(isIdentifierContinue("_".codePointAt(0)!), "'_' should be allowed to continue identifier.");
for (const codePoint of [...otherIDStart, ...otherIdContinue]) {
assert(
isIdentifierContinue(codePoint),
`U+${codePoint.toString(16)} should be allowed to continue identifier.`
);
}
assert(isIdentifierContinue(0x200c), "U+200C (ZWNJ) should be allowed to continue identifier.");
assert(isIdentifierContinue(0x200d), "U+200D (ZWJ) should be allowed to continue identifier.");
});
it("scans this file", async () => {

Просмотреть файл

@ -6,5 +6,5 @@
"types": ["node", "mocha"]
},
"include": ["./**/*.ts"],
"exclude": ["dist", "node_modules", "temp"]
"exclude": ["dist", "node_modules", "temp", "adl-output"]
}

Просмотреть файл

@ -0,0 +1,17 @@
{
"name": "@azure-tools/prettier-plugin-adl",
"entries": [
{
"version": "0.1.1",
"tag": "@azure-tools/prettier-plugin-adl_v0.1.1",
"date": "Tue, 18 May 2021 23:43:31 GMT",
"comments": {
"dependency": [
{
"comment": "Updating dependency \"@azure-tools/adl\" from `0.10.0` to `0.11.0`"
}
]
}
}
]
}

Просмотреть файл

@ -0,0 +1,9 @@
# Change Log - @azure-tools/prettier-plugin-adl
This log was last generated on Tue, 18 May 2021 23:43:31 GMT and should not be manually modified.
## 0.1.1
Tue, 18 May 2021 23:43:31 GMT
_Initial release_

Просмотреть файл

@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

Просмотреть файл

@ -0,0 +1,32 @@
# Prettier Plugin for ADL
## Requirements
- **Using node 14 and above**
- For use in **VSCode**, use version `VSCode 1.56` and above.
## Getting Started
```bash
npm install --save-dev prettier @azure-tools/prettier-plugin-adl
```
You can now call prettier
```bash
./node_modules/.bin/prettier --write '**/*.adl'
```
# Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.microsoft.com.
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

Просмотреть файл

@ -0,0 +1,153 @@
prettier-plugin-adl
THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
Do Not Translate or Localize
This project incorporates components from the projects listed below. The
original copyright notices and the licenses under which Microsoft received such
components are set forth below. Microsoft reserves all rights not expressly
granted herein, whether by implication, estoppel or otherwise.
1. function-bind version 1.1.1 (https://github.com/Raynos/function-bind)
2. has version 1.0.3 (https://github.com/tarruda/has)
3. is-core-module version 2.2.0 (https://github.com/inspect-js/is-core-module)
4. path-parse version 1.0.6 (https://github.com/jbgutierrez/path-parse)
5. resolve version 1.20.0 (https://github.com/browserify/resolve)
%% function-bind NOTICES AND INFORMATION BEGIN HERE
=====================================================
Copyright (c) 2013 Raynos.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
=====================================================");
END OF function-bind NOTICES AND INFORMATION
%% has NOTICES AND INFORMATION BEGIN HERE
=====================================================
Copyright (c) 2013 Thiago de Arruda
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
=====================================================");
END OF has NOTICES AND INFORMATION
%% is-core-module NOTICES AND INFORMATION BEGIN HERE
=====================================================
The MIT License (MIT)
Copyright (c) 2014 Dave Justice
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
=====================================================");
END OF is-core-module NOTICES AND INFORMATION
%% path-parse NOTICES AND INFORMATION BEGIN HERE
=====================================================
The MIT License (MIT)
Copyright (c) 2015 Javier Blanco
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
=====================================================");
END OF path-parse NOTICES AND INFORMATION
%% resolve NOTICES AND INFORMATION BEGIN HERE
=====================================================
MIT License
Copyright (c) 2012 James Halliday
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
=====================================================");
END OF resolve NOTICES AND INFORMATION

Просмотреть файл

@ -0,0 +1,30 @@
{
"name": "@azure-tools/prettier-plugin-adl",
"version": "0.1.1",
"description": "",
"main": "dist/index.js",
"scripts": {
"build": "rollup --config 2>&1 && npm run generate-third-party-notices",
"test": "mocha --timeout 5000 'test/**/*.js'",
"test-official": "mocha --timeout 5000 --forbid-only 'test/**/*.js'",
"generate-third-party-notices": "node ../../eng/scripts/generate-third-party-notices"
},
"author": "Microsoft Corporation",
"license": "MIT",
"dependencies": {
"prettier": "~2.2.1"
},
"devDependencies": {
"@azure-tools/adl": "0.11.0",
"@rollup/plugin-commonjs": "~17.1.0",
"@rollup/plugin-json": "~4.1.0",
"@rollup/plugin-node-resolve": "~11.2.0",
"@rollup/plugin-replace": "~2.4.2",
"mocha": "~8.3.2",
"rollup": "~2.41.4"
},
"files": [
"dist/**/*",
"ThirdPartyNotices.txt"
]
}

Просмотреть файл

@ -0,0 +1,29 @@
// @ts-check
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import json from "@rollup/plugin-json";
import replace from "@rollup/plugin-replace";
export default {
input: "src/index.js",
output: {
file: "dist/index.js",
format: "commonjs",
sourcemap: true,
exports: "default",
},
context: "this",
external: ["fs/promises", "prettier"],
plugins: [
resolve({ preferBuiltins: true }),
commonjs(),
json(),
replace({
values: {
"export const adlVersion = getVersion();": `export const adlVersion = "";`,
},
delimiters: ["", ""],
preventAssignment: true,
}),
],
};

Просмотреть файл

@ -0,0 +1,2 @@
const { ADLPrettierPlugin } = require("@azure-tools/adl");
module.exports = ADLPrettierPlugin;

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше