How to allow optional trailing commas in macros? - macros

Here's a synthetic example of what I want:
macro_rules! define_enum {
($Name:ident { $($Variant:ident),* }) => {
pub enum $Name {
None,
$($Variant),*,
}
}
}
define_enum!(Foo { A, B });
This code compiles, but if add a comma to it:
define_enum!(Foo { A, B, });
// ^
The compilation fails. I can fix it with:
($Name:ident { $($Variant:ident,)* })
// ^
but then define_enum!(Foo { A, B }); fails,
How should I write a macro to handle both cases:
define_enum!(Foo { A, B });
define_enum!(Foo { A, B, });

Handle both cases
You can handle both cases by... handling both cases:
macro_rules! define_enum {
($Name:ident { $($Variant:ident,)* }) => {
pub enum $Name {
None,
$($Variant),*,
}
};
($Name:ident { $($Variant:ident),* }) => {
define_enum!($Name { $($Variant,)* });
};
}
define_enum!(Foo1 { A, B });
define_enum!(Foo2 { A, B, });
fn main() {}
We've moved the main implementation to the version that expects the trailing comma. We then added a second clause that matches the case with the missing comma and rewrites it to the version with a comma.
Make the comma optional
DK. points out an alternative, making the trailing comma itself optional.
This avoids the need to delegate from one implementation to the other.
Rust 1.32
You can use the ? macro repeater to write this and disallow multiple trailing commas:
($Name:ident { $($Variant:ident),* $(,)? }) => {
// ^^^^^
Previous versions
This allows multiple trailing commas:
($Name:ident { $($Variant:ident),* $(,)* }) => {
// ^^^^^

Change the line
($Name:ident { $($Variant:ident),* }) => {
to
($Name:ident { $($Variant:ident),* $(,)? }) => {
to add an optional comma at the end. This works in stable Rust / 2018 edition. This syntax also works for other separators like a semicolon.

Another option (if you're using an Incremental TT Muncher) is to use an optional capture of the delimiter + the remaining tokens. Using a slightly different example:
macro_rules! example {
($name:ident = $value:expr $(, $($tts:tt)*)?) => {
println!("{} = {}", stringify!($name), $value);
example!($($($tts)*)?);
};
($value:expr $(, $($tts:tt)*)?) => {
println!("{}", $value);
example!($($($tts)*)?);
};
() => {};
}
This macro can then be invoked as, for example:
example! { A = 1, "B", C = 3, "D" }
and the trailing comma can be either included or omitted.

Related

AST - Find if object keys have usage

Very very inexperienced with AST parsing, been at it for a day making all rite progress got it working on a single file, but not through imports.
The idea is that i want to tag an object with a comment to flag it for extremely aggressive tree shaking.
In
// (object-method.js)
export const ObjectMethods = /*#__PURE__*/ {
foo: () => "do foo",
bar: () => "do bar"
}
// index.js
import { ObjectMethods } from "./object-methods";
export const runActions = () => {
ObjectMethods .bar();
};
out (foo is deleted as object key because its never used)
// (object-method.js)
export const ObjectMethods = /*#__PURE__*/ {
bar: () => "do bar"
}
// index.js
import { ObjectMethods } from "./object-methods";
export const runActions = () => {
ObjectMethods .bar();
};
Its basically a glorified "usage" detector, just has to be real basic only support objects declared at top-level scope with the comment.
This is what iv'e got up to but then this only works on usage within a single file....
const PURE_IDENTIFIER = "#__PURE__";
export default function() {
return {
visitor: {
ObjectExpression(program) {
if(!program.node.leadingComments) return;
if(program.node.leadingComments.some((comment) => comment.value === PURE_IDENTIFIER)) {
let objectKeys = program.node.properties.map((prop) => prop.key.loc.identifierName);
let found = [];
program.parentPath.parentPath.parentPath.parentPath.traverse({
Identifier(p) {
if(objectKeys.includes(p.node.loc.identifierName)) {
found.push(p.node.loc.identifierName);
}
}
});
let duplicates = found.filter((item, index) => found.indexOf(item) === index && found.lastIndexOf(item) !== index);
program.node.properties = program.node.properties.filter((prop) => {
/* REMOVES DUPLICATES */
return duplicates.includes(prop.key.loc.identifierName)
});
}
},
},
};
}
The question is how can i support multifile usage detection and removal - Thanks

The createClass helper of babeljs make mangle not working

I am using babel as transpiler and I want to mangle some methods with uglifyjs.
Here is a demo:
class A {
methodA() {}
}
And its output by babel:
var A = function () {
function A() {
_classCallCheck(this, A);
}
_createClass(A, [{
key: "methodA",
value: function methodA() {}
}]);
return A;
}();
However when I try to mangle methodA, it does not work. Because methodA in the output is a string.
But the same code output by typescript works, it is not a string:
var A = /** #class */ (function () {
function A() {
}
A.prototype.methodA = function () { };
return A;
}());
So my question is: How can I mangle method name when using babeljs ?
OK, I found the answer.
Just use loose mode:
[ ['#babel/preset-env', { loose: true }] ]
The result will be closer to TS.

How I can call Observable.complete callback function? It doesn't work

In ionic 2, (and angularfire2#4.0.0-rc.0, firebase#3.9.0)
I tried this code, but complete function doesn't work!
constructor(public af: AngularFireDatabase, ...) {
this.temp = af.list('/myTest').subscribe(data => {
... // my tastks
}, error => {
console.log(error);
}, () => {
console.log("done!"); // I can't see this output!
});
}
I already tried very many times, but I can't see "done".
In another function, I tried this but the result is same with first.
console.log(this.temp.closed); // false
this.temp.unsubscribe()
console.log(this.temp.closed); // true
,, What can I do..?
AngularFire list and object observables do not complete. Their internal Firebase refs remain connected, listening for changes to the database.
If you want the list to complete after the first value is received, you can use the first operator:
import "rxjs/add/operator/first";
constructor(public af: AngularFireDatabase, ...) {
this.temp = af.list('/myTest')
.first()
.subscribe(data => {
... // my tastks
}, error => {
console.log(error);
}, () => {
console.log("done!");
});
}
Note that complete handlers are not called upon unsubscription. They are called when the observable completes.
If you want a callback to be invoked upon either completion or unsubscription, you can use the finally operator instead:
import "rxjs/add/operator/finally";
constructor(public af: AngularFireDatabase, ...) {
this.temp = af.list('/myTest')
.finally(() => {
console.log("done!");
})
.subscribe(data => {
... // my tastks
}, error => {
console.log(error);
});
}

Babel: Replacing ArrowFunctionExpression vs FunctionDeclaration in ExportDefaultDeclaration

Say I have the following code I want to transform:
export default () => {};
The following visitor code works:
export default function ({ types: t }) {
return {
visitor: {
ArrowFunctionExpression(path) {
if (!path.node.body.directives.some(d => d.value.value === 'inject')) return;
path.node.body.directives = path.node.body.directives.filter(d => d.value.value !== 'inject');
path.replaceWith(t.arrayExpression([path.node]));
}
}
};
}
That results in the following output:
export default [() => {}];
Great! Now change the input:
export default function () {
'inject';
};
And the visitor:
export default function ({types: t}) {
return {
visitor: {
FunctionDeclaration(path) {
if (!path.node.body.directives.some(d => d.value.value === 'inject')) return;
path.node.body.directives = path.node.body.directives.filter(d => d.value.value !== 'inject');
path.replaceWith(t.arrayExpression([path.node]));
}
}
};
}
That produces the following error:
TypeError: unknown: Property elements[0] of ArrayExpression expected node to be of a type ["null","Expression","SpreadElement"] but instead got "FunctionDeclaration"
Okay, so convert the FunctionDeclaration to a FunctionExpression:
export default function ({types: t}) {
return {
visitor: {
FunctionDeclaration(path) {
if (!path.node.body.directives.some(d => d.value.value === 'inject')) return;
path.node.body.directives = path.node.body.directives.filter(d => d.value.value !== 'inject');
path.replaceWith(
t.arrayExpression([
t.functionExpression(path.node.id, path.node.params, path.node.body, path.node.generator, path.node.async),
])
);
}
}
};
}
And I get the following error:
TypeError: unknown: Property declaration of ExportDefaultDeclaration expected node to be of a type ["FunctionDeclaration","ClassDeclaration","Expression"] but instead got "ExpressionStatement"
And that is where I'm lost. I'm creating an ArrayExpression just as I was with the default-exported arrow function. Why is it complaining about receiving an ExpressionStatement?
Note, the desired output is as such:
export default [function () {
}];

Test resolution is blocked when the expected condition fails

With Protractor, and using Mocha framework, I am comparing two arrays of values, one from a bar chart, one from text fields.
The code looks like this:
it("Each bar should have a value equal to its percentage", () => {
var identicalValue: boolean = false;
helper.getFirstValues(textLocator).then((textValue) => {
helper.getBarValues(barLocator).then((barValue) => {
identicalValue = helper.compareArray(textValue, barValue);
//compareArray returns a boolean, true if the arrays have the same values
expect(identicalValue).to.equal(true);
});
});
});
the functions are coded this way:
public getFirstValues(factsheetTab: protractor.ElementFinder): webdriver.promise.Promise<{}> {
var deferred = protractor.promise.defer();
factsheetTab.all(by.tagName("tr")).map((ele, index) => {
return {
index: index,
text: ele.all(by.tagName("td")).first().getText()
}
}).then((topValue) => {
deferred.fulfill(topValue);
},
(reason) => { deferred.reject(reason) });
return deferred.promise;
};
public getBarValues(factsheetTab: protractor.ElementFinder): webdriver.promise.Promise<{}> {
var deferred = protractor.promise.defer();
factsheetTab.all(by.tagName("tr")).map((ele, index) => {
return {
index: index,
text: ele.all(by.tagName("td")).last().element(by.tagName("div")).getAttribute("data-value")
}
}).then((barValue) => {
deferred.fulfill(barValue);
},
(reason) => { deferred.reject(reason) });
return deferred.promise;
};
My problem is that when the comparison returns false, so when the two arrays have differences, the test is blocked. It doesn't fail, the browser remains opened on that step, and the process stops, ignoring the remaining tasks.
Note: the function helper.compareArray returns a correct value. I could also write "expect(false).to.equal(true)" and get blocked too.
Am I doing something wrong in this test? Do you see a reason why this test is not finished?
edit: I found a workaround, to not get stuck in case of the test failing:
it("Each bar should have a value equal to its percentage", () => {
var identicalValue: boolean = false;
var textValue = null;
helper.getFirstValues(textLocator).then((value) => {
textValue = value;
});
helper.getBarValues(barLocator).then((barValue) => {
chai.assert.deepEqual(barValue, textValue);
});
});
(using #Brine's suggestion, for the deepEqual)
This seems to work, the other tests are ran if this one fails.
I'm still curious to know what was wrong with the first version though.
Not sure if this is a bug in your helper.compareArray or Mocha... but why not compare the arrays directly? I don't use Mocha but it seems something like this would work:
expect(textValue).deepEqual(barValue);